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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues