Expense Tracker MCP
# Expense Tracker MCP
A local [MCP](https://modelcontextprotocol.io) server that lets an LLM (Claude Desktop, the FastMCP Inspector, or any MCP client) track your expenses and credits in a SQLite database on your machine.
Built with [FastMCP](https://gofastmcp.com).
## Features
- Add, list, update, and delete expenses
- Track credits (money received) separately from expenses
- Category / subcategory enforcement — the LLM can only use categories and subcategories you've defined, so entries stay clean and consistent
- Per-category expense summary
- Running balance (`total credited − total spent`)
- All data stored locally in SQLite — nothing leaves your machine
## Project structure
```
expence-tracker-mcp/
├── server.py # entry point used by Claude Desktop / fastmcp CLI
├── src/expence_tracker_mcp/
│ ├── __init__.py # server: tools, resource, DB logic
│ ├── category.json # editable category → subcategory map
│ └── expenses.db # SQLite DB (auto-created on first run, gitignored)
├── pyproject.toml
└── uv.lock
```
## Requirements
- Python >= 3.13
- [uv](https://docs.astral.sh/uv/)
## Setup
```bash
git clone <this-repo>
cd expence-tracker-mcp
uv sync
```
`uv sync` creates a `.venv` and installs `fastmcp` and its dependencies.
## Running the server standalone
```bash
uv run server.py
```
## Running the MCP Inspector (debugger)
Use this to interactively call the tools/resource in a browser before wiring it up to an LLM client.
```bash
uv run fastmcp dev inspector server.py
```
This prints a local URL (with an auth token) — open it in your browser, click **Connect**, then try the tools from the **Tools** tab.
## Connecting to Claude Desktop
Add an entry to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"ExpenseTracker": {
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/expence-tracker-mcp",
"fastmcp",
"run",
"server.py"
]
}
}
}
```
Replace the path with the absolute path to this project on your machine, then fully quit and reopen Claude Desktop (Cmd+Q, not just close the window) to pick up the change.
## Tools
| Tool | Description |
|---|---|
| `add_expense(date, amount, category, subcategory, note="")` | Add a new expense. `category` and `subcategory` are **required** and must come from `category.json` (see below). |
| `list_expenses(category="", start_date="", end_date="")` | List expenses, optionally filtered by category and/or date range (`YYYY-MM-DD`). |
| `update_expense(id, date="", amount=None, category="", subcategory="", note="")` | Update an existing expense. Only the fields you pass are changed. |
| `delete_expense(id)` | Delete an expense by id. |
| `add_credit(date, amount, source="", note="")` | Add credit (money received) to the tracker. |
| `list_credits(start_date="", end_date="")` | List credits, optionally filtered by date range. |
| `summarize_expenses(start_date="", end_date="")` | Totals grouped by category, plus overall `total_spent`. |
| `get_balance()` | Returns `{ total_credited, total_spent, balance }`. |
Every write tool returns `{"status": "ok", ...}` on success or `{"status": "error", "message": "..."}` on failure (e.g. invalid category, record not found) instead of raising — so the calling LLM can see what went wrong and retry.
## Resource
| Resource | Description |
|---|---|
| `data://categories` | The full category → subcategory map from `category.json`. The LLM is instructed (via the resource description) to always pick category/subcategory from here. |
## Customizing categories
Edit `src/expence_tracker_mcp/category.json` — it's read fresh on every tool call, so changes take effect immediately without restarting the server:
```json
{
"food": ["dining out", "snacks"],
"travel": ["cab", "flight", "train", "fuel"],
"rent": ["house rent", "office rent"],
"groceries": []
}
```
- A category **must** be a top-level key in this file, or `add_expense` / `update_expense` will reject it.
- `subcategory` is required on `add_expense`.
- If a category's subcategory list is **non-empty**, the subcategory must be one of those exact values.
- If a category's subcategory list is **empty** (e.g. `"groceries": []`), any non-blank subcategory text is accepted — useful for categories too varied to enumerate.
This enforcement happens server-side in `add_expense`/`update_expense`, so invalid entries are rejected even if the LLM ignores the `data://categories` resource.
## Database
SQLite database at `src/expence_tracker_mcp/expenses.db`, auto-created on first run with two tables:
```sql
CREATE TABLE expenses(
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
amount REAL NOT NULL,
category TEXT NOT NULL,
subcategory TEXT DEFAULT '',
note TEXT DEFAULT ''
);
CREATE TABLE credits(
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
amount REAL NOT NULL,
source TEXT DEFAULT '',
note TEXT DEFAULT ''
);
```
The `.db` file is gitignored — each machine gets its own local data.
## Inspecting the server
Check tool/resource counts without starting a full client:
```bash
uv run fastmcp inspect server.py
```
TDQS
Scored across 8 tools
Each tool targets a distinct resource and action: expenses have add/list/update/delete, credits have add/list, and summarize/get_balance provide reporting. There is no meaningful overlap or ambiguity between tools.
All tools follow a clear verb_noun snake_case pattern. List operations use plural nouns (list_expenses, list_credits) while singular entity operations use singular nouns, which is a predictable and consistent convention.
Eight tools is well-scoped for an expense tracker: full CRUD for expenses, credit tracking, and balance/summary reporting. Each tool earns its place without redundancy or bloat.
Expenses have complete CRUD coverage and reporting is solid, but credits only support add and list with no update or delete. This is a minor gap that agents can work around by re-adding corrected credits, but it is a slight lifecycle gap.