Skip to main content
Glama
MayankKapgate

fitness-tracker-mcp

README.md
# 🏋️ Fitness Tracker — MCP Server

A **fully offline** [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that enables any MCP‑compatible AI client — such as Claude Code, Claude Desktop, or Cursor — to **log workouts**, **track dietary macros**, and **retrieve daily health summaries**, all backed by a local SQLite database with zero network dependency.

---

## 📖 Table of Contents

- [Why This Project?](#-why-this-project)
- [Key Features](#-key-features)
- [Technology Stack](#-technology-stack)
- [Architecture Overview](#-architecture-overview)
- [Data Flow](#-data-flow)
- [Database Schema](#-database-schema)
- [Project Structure](#-project-structure)
- [Getting Started](#-getting-started)
- [Connecting to MCP Clients](#-connecting-to-mcp-clients)
- [Tool Reference (API)](#-tool-reference-api)
- [Example Usage](#-example-usage)
- [Testing](#-testing)
- [Security & Safety](#-security--safety)
- [License](#-license)

---

## 💡 Why This Project?

Large Language Models are great at conversation, but they can't natively *persist* user data across sessions. The **Model Context Protocol** bridges that gap by letting LLMs call external tools — turning the AI into a true assistant that can read, write, and query structured data on the user's behalf.

This project demonstrates a practical MCP integration: a fitness tracker that an AI assistant can operate hands‑free. Ask your AI *"Log a 30‑minute run that burned 300 calories"* and it will validate the data, store it in SQLite, and confirm — all without you ever opening a spreadsheet.

---

## ✨ Key Features

| Feature | Description |
|---|---|
| **Workout Logging** | Record exercise sessions with type, duration, and calories burned |
| **Macro Tracking** | Log daily protein, carbs, and fat intake per meal or per day |
| **Daily Summaries** | Aggregated view of workouts + nutrition with calorie calculations |
| **Fully Offline** | Stdio transport — no network calls, no API keys, no cloud dependency |
| **Strict Validation** | Pydantic v2 schemas catch malformed LLM outputs before they reach the DB |
| **SQL Injection Safe** | Parameterised queries throughout — user input never touches raw SQL |
| **Comprehensive Tests** | 22 Pytest cases covering schema validation, DB logic, and edge cases |

---

## 🧱 Technology Stack

| Layer | Technology | Purpose |
|---|---|---|
| **MCP Framework** | [FastMCP](https://github.com/jlowin/fastmcp) | Exposes Python functions as MCP tools over stdio |
| **Database** | SQLite 3 | Lightweight, zero‑config local persistence |
| **Validation** | Pydantic v2 | Schema enforcement and type coercion for LLM inputs |
| **Testing** | Pytest | Isolated, per‑test in‑memory databases |
| **Language** | Python 3.10+ | Core runtime |

---

## 🏗️ Architecture Overview

The system follows a **layered architecture** with clear separation of concerns:

```mermaid
graph TB
    subgraph Client Layer
        A["🤖 MCP Client<br/>(Claude Code / Claude Desktop / Cursor)"]
    end

    subgraph Transport Layer
        B["📡 stdio<br/>(JSON-RPC over stdin/stdout)"]
    end

    subgraph MCP Server ["MCP Server (server.py)"]
        direction TB
        C["🔧 FastMCP Tool Router<br/>Routes tool calls to handlers"]
        D["📋 Pydantic Schemas<br/>WorkoutInput · MacrosInput · DailySummaryRequest"]
        E["⚙️ Core Business Logic<br/>insert_workout · insert_macros · fetch_daily_summary"]
        F["🗄️ Database Layer<br/>get_connection · init_db"]
    end

    subgraph Storage
        G[("💾 SQLite<br/>fitness_tracker.db")]
    end

    A <-->|"JSON-RPC"| B
    B <-->|"Tool calls & responses"| C
    C --> D
    D -->|"Validated data"| E
    E <--> F
    F <--> G

    style A fill:#4A90D9,stroke:#2C5F8A,color:#fff
    style B fill:#F5A623,stroke:#C77E1A,color:#fff
    style C fill:#7B68EE,stroke:#5A4DB2,color:#fff
    style D fill:#50C878,stroke:#3A9458,color:#fff
    style E fill:#FF6B6B,stroke:#CC5555,color:#fff
    style F fill:#DDA0DD,stroke:#AA70AA,color:#fff
    style G fill:#87CEEB,stroke:#5F9EAF,color:#000
```

### Layer Responsibilities

| Layer | Component | Responsibility |
|---|---|---|
| **Client** | Claude Code / Desktop | Sends natural language → MCP tool calls |
| **Transport** | stdio (JSON-RPC) | Serialises tool calls over stdin/stdout — no HTTP, no ports |
| **Router** | FastMCP | Matches incoming tool names to Python handler functions |
| **Validation** | Pydantic Schemas | Parses & validates every input field *before* DB access |
| **Business Logic** | Core Functions | Executes inserts, aggregations, and calorie calculations |
| **Storage** | SQLite via `sqlite3` | Persists data in a single `fitness_tracker.db` file |

---

## 🔄 Data Flow

A step‑by‑step trace of what happens when a user says *"Log a 30‑minute run"*:

```mermaid
sequenceDiagram
    participant User
    participant Client as MCP Client (Claude)
    participant Transport as stdio (JSON-RPC)
    participant Router as FastMCP Router
    participant Schema as Pydantic Validator
    participant Logic as Business Logic
    participant DB as SQLite DB

    User->>Client: "Log a 30-minute run that burned 300 calories"
    Client->>Transport: tool_call: log_workout(date, type, duration, calories)
    Transport->>Router: Deserialize JSON-RPC request
    Router->>Schema: WorkoutInput(date, type, duration, calories)

    alt Validation Fails
        Schema-->>Router: ❌ ValidationError (clear message)
        Router-->>Transport: Error response
        Transport-->>Client: Display error to user
    end

    Schema-->>Router: ✅ Validated WorkoutInput object
    Router->>Logic: insert_workout(validated_data)
    Logic->>DB: INSERT INTO workouts (date, type, duration, calories) VALUES (?, ?, ?, ?)
    DB-->>Logic: Row ID
    Logic-->>Router: {status: success, workout: {...}}
    Router-->>Transport: JSON-RPC response
    Transport-->>Client: "Logged: 30 min running — 300 kcal burned ✅"
    Client-->>User: Confirmation message
```

---

## 🗃️ Database Schema

The SQLite database (`fitness_tracker.db`) is auto‑created on first run and contains two tables:

```mermaid
erDiagram
    WORKOUTS {
        INTEGER id PK "Auto-increment"
        TEXT date "YYYY-MM-DD (NOT NULL)"
        TEXT type "e.g. running, cycling (NOT NULL)"
        REAL duration "Minutes, > 0 (NOT NULL)"
        REAL calories "kcal burned, >= 0 (NOT NULL)"
    }

    MACROS {
        INTEGER id PK "Auto-increment"
        TEXT date "YYYY-MM-DD (NOT NULL)"
        REAL protein "Grams, >= 0 (NOT NULL)"
        REAL carbs "Grams, >= 0 (NOT NULL)"
        REAL fat "Grams, >= 0 (NOT NULL)"
    }
```

### Calorie Calculation

The daily summary computes **estimated calories consumed** from macros using standard Atwater factors:

$$\text{Calories} = (\text{Protein} \times 4) + (\text{Carbs} \times 4) + (\text{Fat} \times 9) \;\text{kcal}$$

---

## 📂 Project Structure

```
MCP_Project/
├── server.py              # MCP server — tools, schemas, DB helpers, entrypoint
├── test_server.py         # Pytest suite (22 tests across 6 test classes)
├── requirements.txt       # Python dependencies (fastmcp, pydantic, pytest)
├── fitness_tracker.db     # SQLite database (auto-created on first run)
├── .gitignore             # Ignores venv, __pycache__, .env
├── .env                   # Environment variables (git-ignored)
└── README.md              # This file
```

### File Breakdown

| File | Lines | Description |
|---|---|---|
| `server.py` | ~322 | Complete MCP server: DB init, Pydantic models, CRUD operations, FastMCP tool definitions, and stdio entrypoint |
| `test_server.py` | ~265 | 22 tests in 6 classes — schema validation (valid + invalid inputs), DB inserts, daily aggregation, date isolation, and SQL injection safety |
| `requirements.txt` | 3 | `fastmcp`, `pydantic`, `pytest` |

---

## 🚀 Getting Started

### Prerequisites

- **Python 3.10+** installed
- **pip** package manager

### 1. Clone the Repository

```bash
git clone https://github.com/MayankKapgate/fitness-tracker-mcp.git
cd MCP_Project
```

### 2. Create & Activate a Virtual Environment (Recommended)

```bash
# Windows
python -m venv myvenv
myvenv\Scripts\activate

# macOS / Linux
python3 -m venv myvenv
source myvenv/bin/activate
```

### 3. Install Dependencies

```bash
pip install -r requirements.txt
```

### 4. Run the Test Suite

```bash
pytest test_server.py -v
```

You should see **22 tests pass** ✅

### 5. Start the Server (Standalone)

```bash
python server.py
```

> **Note:** The server uses **stdio transport** — it reads JSON‑RPC from `stdin` and writes to `stdout`. You won't see a shell prompt; this is by design for MCP client consumption.

---

## 🔌 Connecting to MCP Clients

### Claude Code

Register the server once from your terminal:

```bash
claude mcp add fitness-tracker --transport stdio -- python server.py
```

> **Tip:** If Claude Code isn't launched from the project directory, use the full path:
>
> ```bash
> claude mcp add fitness-tracker --transport stdio -- python "C:\Users\Mayan\OneDrive\Documents\MCP_Project\server.py"
> ```

### Claude Desktop

Add the following to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "fitness-tracker": {
      "command": "python",
      "args": ["C:\\Users\\Mayan\\OneDrive\\Documents\\MCP_Project\\server.py"],
      "transport": "stdio"
    }
  }
}
```

### Other MCP Clients

Any MCP‑compatible client can connect using:
- **Transport:** `stdio`
- **Command:** `python server.py` (or full path to `server.py`)

---

## 🛠️ Tool Reference (API)

The server exposes **3 MCP tools**:

### 1. `log_workout`

Log a single workout session.

| Parameter | Type | Constraints | Example |
|---|---|---|---|
| `date` | `string` | ISO 8601 (`YYYY-MM-DD`) | `"2026-08-04"` |
| `type` | `string` | 1–100 characters | `"running"` |
| `duration` | `float` | > 0 (minutes) | `30.0` |
| `calories` | `float` | ≥ 0 (kcal) | `300.0` |

**Returns:**
```json
{
  "status": "success",
  "workout": {
    "id": 1,
    "date": "2026-08-04",
    "type": "running",
    "duration": 30.0,
    "calories": 300.0
  }
}
```

---

### 2. `log_macros`

Log dietary macronutrients for a meal or full day.

| Parameter | Type | Constraints | Example |
|---|---|---|---|
| `date` | `string` | ISO 8601 (`YYYY-MM-DD`) | `"2026-08-04"` |
| `protein` | `float` | ≥ 0 (grams) | `150.0` |
| `carbs` | `float` | ≥ 0 (grams) | `200.0` |
| `fat` | `float` | ≥ 0 (grams) | `60.0` |

**Returns:**
```json
{
  "status": "success",
  "macros": {
    "id": 1,
    "date": "2026-08-04",
    "protein": 150.0,
    "carbs": 200.0,
    "fat": 60.0
  }
}
```

---

### 3. `get_daily_summary`

Retrieve a combined workout and nutrition summary for a given date.

| Parameter | Type | Constraints | Example |
|---|---|---|---|
| `date` | `string` | ISO 8601 (`YYYY-MM-DD`) | `"2026-08-04"` |

**Returns:**
```json
{
  "date": "2026-08-04",
  "workouts": {
    "count": 2,
    "entries": [
      {"id": 1, "date": "2026-08-04", "type": "running", "duration": 30.0, "calories": 300.0},
      {"id": 2, "date": "2026-08-04", "type": "weights", "duration": 45.0, "calories": 250.0}
    ],
    "total_duration_min": 75.0,
    "total_calories_burned": 550.0
  },
  "macros": {
    "count": 1,
    "entries": [
      {"id": 1, "date": "2026-08-04", "protein": 150.0, "carbs": 200.0, "fat": 60.0}
    ],
    "total_protein_g": 150.0,
    "total_carbs_g": 200.0,
    "total_fat_g": 60.0,
    "total_calories_consumed": 1940.0
  }
}
```

---

## 💬 Example Usage

Once connected, simply chat with your AI assistant naturally:

| You Say | Tool Called | What Happens |
|---|---|---|
| *"I did a 30‑minute run and burned 300 calories"* | `log_workout` | Stores the workout for today's date |
| *"Log my lunch: 40g protein, 60g carbs, 15g fat"* | `log_macros` | Records one macro entry |
| *"How did I do today?"* | `get_daily_summary` | Returns aggregated totals for the current date |
| *"What was my workout on August 4th?"* | `get_daily_summary` | Fetches data for `2026-08-04` |

---

## 🧪 Testing

The test suite (`test_server.py`) contains **22 tests** across **6 test classes**, using isolated temporary SQLite databases per test:

| Test Class | Tests | What It Covers |
|---|---|---|
| `TestWorkoutSchema` | 10 | Valid workout, bad dates, negative/zero duration, negative calories, empty/too-long type, missing fields, wrong types |
| `TestMacrosSchema` | 6 | Valid macros, invalid date, negative protein/carbs/fat, missing fields |
| `TestDailySummarySchema` | 2 | Valid request, garbage date |
| `TestWorkoutDB` | 3 | Insert & retrieve, multiple inserts, SQL injection safety |
| `TestMacrosDB` | 2 | Insert & retrieve, SQL injection via date field |
| `TestDailySummary` | 3 | Empty day, populated day with aggregation, cross-date isolation |

### Running Tests

```bash
# Run all tests with verbose output
pytest test_server.py -v

# Run a specific test class
pytest test_server.py::TestWorkoutSchema -v

# Run with coverage (requires pytest-cov)
pip install pytest-cov
pytest test_server.py --cov=server --cov-report=term-missing
```

---

## 🔒 Security & Safety

| Concern | Mitigation |
|---|---|
| **SQL Injection** | All database queries use parameterised `?` placeholders — user input is never interpolated into SQL strings |
| **Malformed LLM Output** | Every tool input passes through a Pydantic v2 schema with strict field validators before reaching the database |
| **Date Validation** | Custom `@field_validator` ensures ISO 8601 compliance; garbage strings like `"yesterday"` or `"'; DROP TABLE"` are rejected |
| **Type Coercion** | Pydantic's strict mode catches truly incompatible types (e.g., `"slow"` for a `float` field) |
| **Network Exposure** | stdio transport — zero network traffic, no open ports, no API keys required |
| **Data Privacy** | All data stays in a local `fitness_tracker.db` file on your machine — nothing leaves your system |

---

## 📝 License

MIT — use freely.