Skip to main content
Glama
Mannu-Thakur

Expense MCP Server

by Mannu-Thakur
README.md
# πŸ’Έ Expense MCP Server

A production-ready **Model Context Protocol (MCP) server** for managing personal expenses β€” built with FastMCP, SQLAlchemy, and Pydantic. Deploys as a Docker container on Render with PostgreSQL, and connects to Claude Desktop, Cursor, VS Code, or any MCP-compatible client.

---

## Status

βœ… **Live on Render** β€” Docker Β· PostgreSQL Β· Streamable HTTP

| | |
|---|---|
| **Transport** | Streamable HTTP |
| **Database** | PostgreSQL (Render managed) |
| **Health endpoint** | `/health` |
| **MCP endpoint** | `/mcp` |

---

## Architecture

```
Claude Desktop / Cursor / VS Code MCP
            β”‚
            β”‚  MCP over Streamable HTTP
            β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  Expense MCP Server β”‚  Docker Β· Render
   β”‚  FastMCP + Uvicorn  β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   Expense Service   β”‚  Business logic
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚     Repository      β”‚  Data access layer
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  SQLAlchemy ORM     β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  PostgreSQL / SQLiteβ”‚  Prod / Dev
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

### API Request Flow

```
Claude  β†’  MCP Tool  β†’  FastMCP  β†’  Expense Service  β†’  Repository  β†’  SQLAlchemy  β†’  PostgreSQL
```

---

## ✨ Features

- **8 MCP Tools** β€” add, list, update, delete, search expenses, and get monthly/category/merchant summaries
- **Dual transport** β€” STDIO (local Claude Desktop) and Streamable HTTP (remote/Docker/Render)
- **Dual database** β€” SQLite for local development, PostgreSQL for production
- **Production-ready** β€” structured logging, custom exceptions, connection pooling, non-root Docker user
- **31 tests** β€” full repository, service, and tool coverage with in-memory SQLite isolation
- **One-command deploy** β€” `render.yaml` Blueprint auto-provisions PostgreSQL + Docker web service

### Production Features

| Feature | Detail |
|---|---|
| βœ“ Docker multi-stage image | Minimal, hardened runtime layer |
| βœ“ PostgreSQL | Render-managed with connection pooling |
| βœ“ Render Blueprint | Auto-provisions all infrastructure |
| βœ“ Health endpoint | `/health` for Render and load balancer checks |
| βœ“ Remote HTTP MCP | Accessible at `/mcp` from any MCP client |
| βœ“ Connection pooling | `pool_size=5`, `max_overflow=10`, `pool_recycle=1800` |
| βœ“ Structured logging | ISO timestamps, log levels, logger names |
| βœ“ Non-root container | `appuser` with no escalation |
| βœ“ Automatic DB provisioning | Tables created on first startup |

---

## Compatible Clients

βœ… Claude Desktop  
βœ… Cursor  
βœ… VS Code MCP extension  
βœ… Any MCP client supporting Streamable HTTP

---

## Transport Modes

| Transport | When to use |
|---|---|
| `stdio` | Local Claude Desktop β€” server runs as a child process |
| `streamable-http` | Remote deployment, Docker, Render β€” server listens on HTTP |

---

## πŸ—‚ Project Structure

```
expense-mcp-server/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ config.py              # Pydantic Settings (reads from .env)
β”‚   β”œβ”€β”€ exceptions.py          # ExpenseNotFoundError, ExpenseValidationError
β”‚   β”œβ”€β”€ logging_config.py      # Structured logging setup
β”‚   β”œβ”€β”€ mcp_instance.py        # FastMCP instance + /health endpoint
β”‚   β”œβ”€β”€ server.py              # Registers all 8 tools
β”‚   β”œβ”€β”€ database/
β”‚   β”‚   β”œβ”€β”€ db.py              # Engine, SessionLocal, validate_connection()
β”‚   β”‚   └── models.py          # Expense SQLAlchemy model
β”‚   β”œβ”€β”€ schemas/
β”‚   β”‚   └── expense.py         # ExpenseCreate, ExpenseUpdate (Pydantic)
β”‚   β”œβ”€β”€ repositories/
β”‚   β”‚   └── expense_repository.py
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   └── expense_service.py
β”‚   β”œβ”€β”€ tools/
β”‚   β”‚   β”œβ”€β”€ add_expense.py
β”‚   β”‚   β”œβ”€β”€ list_expenses.py
β”‚   β”‚   β”œβ”€β”€ update_expense.py
β”‚   β”‚   β”œβ”€β”€ delete_expense.py
β”‚   β”‚   β”œβ”€β”€ search_expenses.py
β”‚   β”‚   β”œβ”€β”€ monthly_summary.py
β”‚   β”‚   β”œβ”€β”€ category_summary.py
β”‚   β”‚   └── top_merchants.py
β”‚   └── utils/
β”‚       β”œβ”€β”€ dates.py
β”‚       └── currency.py
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ conftest.py            # In-memory SQLite fixtures + monkeypatch
β”‚   └── test_expense.py        # 31 tests
β”œβ”€β”€ run.py                     # Entry point
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ Dockerfile                 # Multi-stage Docker build
β”œβ”€β”€ docker-compose.yml         # App + PostgreSQL local stack
β”œβ”€β”€ render.yaml                # Render Blueprint (PostgreSQL + Docker web service)
└── .env                       # Local environment variables
```

---

## πŸ›  MCP Tools

| Tool | Description |
|---|---|
| `add_expense` | Add a new expense with amount, category, date, merchant, currency |
| `list_expenses` | List all expenses sorted by date descending |
| `update_expense` | Update any field of an existing expense by ID |
| `delete_expense` | Delete an expense by ID |
| `search_expenses` | Search by category, description, or merchant |
| `monthly_summary` | Total, count, average, and max for the current month |
| `category_summary` | Spending totals grouped by category |
| `top_merchants` | Top merchants by total spending |

---

## βš™οΈ Environment Variables

| Variable | Default | Description |
|---|---|---|
| `APP_ENV` | `development` | `development` or `production` |
| `LOG_LEVEL` | `INFO` | Python log level |
| `MCP_SERVER_NAME` | `Expense Tracker` | Name shown in Claude Desktop |
| `TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
| `HOST` | `0.0.0.0` | Bind host (HTTP mode only) |
| `PORT` | `8000` | Bind port for local/Docker. **Render injects `PORT=10000` automatically β€” no manual configuration needed.** |
| `DATABASE_URL` | `sqlite:///./expenses.db` | SQLite or PostgreSQL connection string |

---

## πŸš€ Getting Started

### Option A β€” Local Development (STDIO + SQLite)

**1. Clone and create a virtual environment**

```bash
git clone https://github.com/your-username/expense-mcp-server.git
cd expense-mcp-server
python -m venv .venv
```

**2. Activate the virtual environment**

```bash
# Windows
.venv\Scripts\activate

# macOS / Linux
source .venv/bin/activate
```

**3. Install dependencies**

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

**4. Configure `.env`**

```env
APP_ENV=development
LOG_LEVEL=INFO
MCP_SERVER_NAME=Expense Tracker
TRANSPORT=stdio
HOST=0.0.0.0
PORT=8000
DATABASE_URL=sqlite:///./expenses.db
```

**5. Run the server**

```bash
python run.py
```

The server starts in STDIO mode β€” ready to be used with Claude Desktop.

---

### Option B β€” Local Docker + PostgreSQL (HTTP)

**1. Start the full stack**

```bash
docker-compose up --build
```

This starts:
- `expense_postgres` β€” PostgreSQL 16 on port 5432
- `expense_mcp` β€” MCP server on port 8000 (Streamable HTTP transport)

The app waits for PostgreSQL to pass its health check before starting.

**2. Verify it's running**

```bash
curl http://localhost:8000/health
```

---

### Option C β€” Deploy to Render (Docker + PostgreSQL)

The project deploys as a **Docker multi-stage image** on Render.

**1. Push your repo to GitHub**

**2. Go to [render.com](https://render.com) β†’ New β†’ Blueprint**

**3. Connect your GitHub repo** β€” Render auto-detects `render.yaml` and automatically creates:
- A **managed PostgreSQL database**
- A **Docker web service** running `run.py`
- All required **environment variables**
- **Database connection** wired via `fromDatabase.connectionString`

**4. Your server will be live at:**
```
https://<your-render-service>.onrender.com
```

> Replace `<your-render-service>` with your actual Render service name (shown in the dashboard after deployment).

---

## πŸ” Health Check

The server exposes `/health` for Render and load balancer health checks.

```bash
curl https://<your-render-service>.onrender.com/health
```

**Response:**
```json
{"status": "ok"}
```

---

## πŸ”Œ Verify the MCP Endpoint

```
GET https://<your-render-service>.onrender.com/mcp
```

> **Note:** A browser will show `405 Method Not Allowed`. This is expected β€” `/mcp` speaks the MCP protocol (not plain HTTP GET). Use an MCP client to connect.

---

## πŸ–₯ Claude Desktop Configuration

Add to `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or  
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS).

### Option A β€” Local STDIO

```json
{
  "mcpServers": {
    "expense-tracker": {
      "command": "C:\\path\\to\\expense-mcp-server\\.venv\\Scripts\\python.exe",
      "args": ["C:\\path\\to\\expense-mcp-server\\run.py"]
    }
  }
}
```

### Option B β€” Render (Remote HTTP)

```json
{
  "mcpServers": {
    "expense-tracker": {
      "type": "streamable-http",
      "url": "https://<your-render-service>.onrender.com/mcp"
    }
  }
}
```

### Option C β€” Docker (Local HTTP)

```json
{
  "mcpServers": {
    "expense-tracker": {
      "type": "streamable-http",
      "url": "http://localhost:8000/mcp"
    }
  }
}
```

> You can include all three entries simultaneously β€” just give each a unique key.

---

## πŸ§ͺ Running Tests

```bash
python -m pytest tests/ -v
```

Expected output:

```
31 passed in ~1s
```

The test suite covers:
- `TestExpenseRepository` β€” 13 tests (CRUD + search + summaries)
- `TestExpenseService` β€” 10 tests (business logic + error handling)
- `TestTools` β€” 8 tests (MCP tool integration)

Tests use an **in-memory SQLite database** with full transaction rollback isolation between each test.

---

## πŸ—ƒ Database

### SQLite (Local)

Zero configuration. The `expenses.db` file is created automatically on first run.

```env
DATABASE_URL=sqlite:///./expenses.db
```

### PostgreSQL (Docker / Production)

```env
DATABASE_URL=postgresql://expense_user:expense_pass@localhost:5432/expense_db
```

The `Expense` table is created automatically via `Base.metadata.create_all()` on startup.

| Column | Type | Notes |
|---|---|---|
| `id` | Integer | Primary key |
| `amount` | Float | Required, must be > 0 |
| `category` | String | Required |
| `description` | String | Optional |
| `merchant` | String | Optional |
| `payment_method` | String | Optional |
| `currency` | String | Default: `INR` |
| `expense_date` | Date | Required |
| `created_at` | DateTime | Auto-set to UTC now |

---

## πŸ“¦ Tech Stack

| Layer | Library | Version |
|---|---|---|
| MCP Framework | `mcp` (FastMCP) | 1.28.1 |
| Settings | `pydantic-settings` | 2.14.2 |
| ORM | `SQLAlchemy` | 2.0.51 |
| Validation | `pydantic` | 2.13.4 |
| PostgreSQL driver | `psycopg2-binary` | 2.9.10 |
| HTTP server | `uvicorn` + `starlette` | β€” |
| Testing | `pytest` | 9.1.1 |

---

## πŸ“„ License

MIT