budget-mcp
MCP server for budget tooling, built on the official MCP Python SDK.
Project layout
budget-mcp/
├── pyproject.toml
├── budget.db # SQLite db, created by init_db.py (gitignored)
├── src/
│ └── budget_mcp/
│ ├── __init__.py
│ ├── server.py # MCPServer instance + tools
│ ├── db.py # schema DDL + connection helper
│ ├── init_db.py # resets budget.db and seeds example rows
│ ├── money.py # dollars <-> integer-cents conversion
│ ├── dates.py # date validation + period resolution
│ ├── categorizer.py # LLM-based transaction categorizer
│ └── eval_categorizer.py # accuracy check against a labeled set
├── tests/
│ ├── conftest.py # puts src/ on sys.path for test collection
│ └── test_tools.py # end-to-end tool tests, see "Tests" below
└── .venv/ # local virtualenv (gitignored)
Requirements
- Python 3.10+ (this project was set up with Python 3.12 via Homebrew:
brew install [email protected])
Setup
cd budget-mcp
/opt/homebrew/bin/python3.12 -m venv .venv
.venv/bin/pip install -e ".[dev]"
PYTHONPATH=src .venv/bin/python -m budget_mcp.init_db # creates + seeds budget.db
macOS + iCloud Drive gotcha: if
~/Desktopis synced via iCloud Drive,the editable install's_editable_impl_budget_mcp.pthfile in.venv/lib/python3.12/site-packages/can end up with the macOS "hidden"file flag set, which makes Python 3.12 skip it — a bareimport budget_mcp(e.g. in a REPL or test) will fail withModuleNotFoundErroreven thoughthe install "succeeded". If that happens, run:chflags nohidden .venv/lib/python3.12/site-packages/_editable_impl_budget_mcp.pthThis doesn't affect running the server or the init script below — both areinvoked with
PYTHONPATH=src, which sidesteps the editable-installmechanism entirely.
categorize_transaction calls the Anthropic API, so it needs a key:
export ANTHROPIC_API_KEY=sk-ant-...
Add the same variable to the env block in the Claude Desktop config below soit's available when Claude Desktop launches the server.
Database
Three tables (see src/budget_mcp/db.py for the full DDL):
budgets(category PK, monthly_limit)transactions(id, date, amount, category -> budgets.category, source, note)savings_goals(id, name, target_amount, current_amount, account_type)
All money columns are integer cents (e.g. 50000 == $500.00) to avoidfloat rounding — SQLite has no real DECIMAL type. transactions.amount issigned: negative = expense, positive = income.
Re-run PYTHONPATH=src .venv/bin/python -m budget_mcp.init_db any time towipe budget.db and reset it to the seed data.
Tools
ping()— placeholder, returns"pong".add_transaction(date, amount, category, source, note=None)— insertsa transaction and returns it plus the category's running total for thattransaction's calendar month.amountis dollars (e.g.-45.67);datemust beYYYY-MM-DD;categorymust already exist inbudgets. Rejectsbad dates, zero/non-finite/sub-cent amounts, unknown categories, and emptysourcewith a clear error message (no stack traces).get_spending_summary(period, group_by, start_date=None, end_date=None)— sums expense transactions (amount < 0) overperiod("this_month","last_month", or"custom"withstart_date/end_date), grouped by"category"or"source", and returns each group's total plus % of totalspend. Rejects unknownperiod/group_byvalues and missing/invalidcustom-range dates.categorize_transaction(description)— classifies a raw statement line(e.g."TESCO STORES 3421 LONDON") into one ofrent,food,transport,savings,business_expense,entertainment,other, via a Claude Haiku4.5 call with structured JSON output (category,confidence,reasoning).The returned category is checked against the fixed list before beingreturned — an invalid category from the model surfaces as an error ratherthan being trusted. RequiresANTHROPIC_API_KEY(see Setup above).
Tests
.venv/bin/pytest tests/ -v
Each test spawns a real server subprocess and drives it through the actualMCP protocol (same path Claude Desktop uses), against a fresh throwawaySQLite db (via BUDGET_MCP_DB_PATH) — your real budget.db is nevertouched. Covers both tools' happy paths (including that running totals andspend percentages come out exact, not float-drifted) and every validationrejection (bad dates, zero/non-finite/sub-cent amounts, unknown categories,empty source, bad period/group_by, missing custom-range dates).
categorize_transaction isn't in this suite — LLM output isn't deterministic,so it doesn't belong in a pass/fail unit test. Instead, check its accuracyagainst 15 hand-labeled realistic statement lines:
PYTHONPATH=src .venv/bin/python -m budget_mcp.eval_categorizer
This calls the live API (needs ANTHROPIC_API_KEY) and prints a pass/failper example plus overall accuracy.
Running locally
Run the server directly (it speaks MCP over stdio):
PYTHONPATH=src .venv/bin/python -m budget_mcp.server
It will sit waiting for an MCP client to talk to it over stdin/stdout — that'sexpected, it's not meant to be run interactively.
To poke at it with the official inspector UI instead:
PYTHONPATH=src .venv/bin/mcp dev src/budget_mcp/server.py
Connecting to Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json and addan mcpServers entry:
{
"mcpServers": {
"budget-mcp": {
"command": "/Users/hamza/Desktop/projects/budget-mcp/.venv/bin/python",
"args": ["-m", "budget_mcp.server"],
"env": {
"PYTHONPATH": "/Users/hamza/Desktop/projects/budget-mcp/src",
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
}
}
Then fully quit and reopen Claude Desktop. In a new conversation, the toolsshould be available (look for the tools/hammer icon) — try asking Claude tocall ping, then to add a transaction or get a spending summary.
Notes
- Uses MCP Python SDK v2 (
mcp.server.MCPServer, formerlyFastMCPinv1.x). Requiresmcp>=1.2.0perpyproject.toml, but what's actuallyinstalled here is the current 2.x line. - Tool validation errors are raised as
mcp.server.mcpserver.exceptions.ToolError, which the framework returnsto the client as a plain error message — never a Python traceback.