LedgerMCP
Keep an eye on your rent ledger — and cross-examine it against your lease — through your AI assistant.
LedgerMCP is a Model Context Protocol serverbuilt for renters. Import your property/rent-portal ledger (CSV, TSV, Excel)and your lease (PDF), and LedgerMCP gives an AI assistant a set ofdeterministic tools to answer questions like:
- "Am I being charged the right rent every month?"
- "Is this 'Valet Trash' fee actually in my lease?"
- "Did they charge me the late fee my lease specifies — or more?"
- "What have I paid this year, and what's my current balance?"
The assistant answers by calling tools that do exact arithmetic in Python(every amount is a Decimal,never a float) and by comparing your ledger line-by-line to your lease —instead of eyeballing a table in its context window and guessing.
Why
Ask an LLM to "total my fees for Q2" or "check my rent against my lease" from apasted statement and you get plausible, confidently-wrong numbers. LedgerMCPmoves the numbers and the comparisons out of the prompt and behind tools:
- The model decides what to ask (
check_rent_charges,find_unexpected_charges, …). - LedgerMCP decides what the answer is, deterministically, in Decimal — andshows its work by quoting the exact lease text it relied on.
Features
- Renter-first workflow — import a rent ledger and a lease, then cross-examine.
- Lease parsing (PDF) — heuristically extracts rent, deposit, term dates,due day, late-fee policy, recurring fees and parties, each with a confidencelevel and the source excerpt. The full lease text is retained so theassistant can read anything the heuristics miss.
- Deterministic cross-examination — rent checks, unexpected-charge audits,deposit and late-fee comparisons, all exact.
- Pluggable ledger parsers — CSV/TSV and the Excel "Full Ledger" rent-portalexport out of the box; add QuickBooks/OFX/QIF/Xero by writing one small class.
- SQLite storage with lossless Decimal (amounts stored as integer cents).
- Classic analytics too — balances, category/monthly rollups, incomestatement, balance sheet.
- MCP server (FastMCP) + a Typer CLI over one shared service layer.
- Idempotent ledger imports, high test coverage, Ruff, Pyright, GitHub Actions CI.
MCP tools
Lease cross-examination
| Tool | Description |
|---|---|
lease_ledger_report |
One-call cross-examination: lease terms + every check + human-readable flags. |
check_rent_charges |
Compare each month's rent charges to the lease's monthly rent. |
find_unexpected_charges |
Classify every charge category as expected / referenced / mismatch / unexpected. |
check_security_deposit |
Compare the deposit charged to the lease deposit. |
check_late_fees |
Compare late fees charged to the lease late-fee policy. |
get_lease_summary |
Extracted lease terms, each with confidence + source excerpt. |
get_lease_text |
Read the raw lease, or just the lines matching a query (pet rules, subletting…). |
Ledger analytics
| Tool | Description |
|---|---|
search_transactions |
Filter by date, category, type, account, source, text, amount range. |
get_account_balance |
Running balance for an account or the whole ledger, optionally as-of a date. |
sum_transactions |
Net / inflow / outflow totals for a filtered set. |
monthly_spending |
Spending grouped by calendar month. |
transactions_by_category |
Per-category rollup, ranked by spend. |
get_income_statement |
Accrual-basis P&L for a period. |
get_balance_sheet |
Assets / liabilities / equity as of a date. |
list_accounts |
The chart of accounts and each account's type. |
Installation
LedgerMCP uses uv.
git clone https://github.com/luke-nielsen/ledger-mcp.git
cd ledger-mcp
uv sync
Quickstart
# 1. Import your rent ledger
uv run ledger-mcp import examples/sample_ledger.csv --db ledger.db
# 2. Import your lease
uv run ledger-mcp import-lease examples/sample_lease.pdf --db ledger.db
# 3. See what the lease says
uv run ledger-mcp lease --db ledger.db
# 4. Cross-examine ledger vs lease
uv run ledger-mcp lease-report --db ledger.db
The bundled example intentionally shows a clean rent match, a late-feediscrepancy (lease says $75, ledger charged $50), and coverage gaps (a depositand pet rent that were never charged):
LEASE vs LEDGER
Findings:
• 'Late Fee' charged $50.00 but lease specifies $75.00.
• Lease items not seen in the ledger: Security Deposit, Pet Rent.
• Late fees charged $50.00 but lease late fee is $75.00.
Rent check:
2026-04 charged $1,450.00 expected $1,450.00 [match]
2026-05 charged $1,450.00 expected $1,450.00 [match]
2026-06 charged $1,450.00 expected $1,450.00 [match]
You can also import a real Excel rent-portal statement and your real lease:
uv run ledger-mcp import "full_ledger.xlsx" --db ledger.db
uv run ledger-mcp import-lease "my_lease.pdf" --db ledger.db
Use it with an AI assistant (the point)
Run the MCP server and point any MCP client at it. For Claude Desktop, add toclaude_desktop_config.json:
{
"mcpServers": {
"ledger": {
"command": "uv",
"args": ["run", "ledger-mcp", "serve"],
"cwd": "/absolute/path/to/ledger-mcp",
"env": { "LEDGER_MCP_DB": "/absolute/path/to/ledger.db" }
}
}
}
Then ask: "Does my ledger match my lease?" — the assistant callslease_ledger_report, relays the flags, and can drill in with check_rent_chargesor get_lease_text to quote the exact clause.
Programmatic use
from ledger_mcp import LedgerService, Settings
service = LedgerService.from_settings(Settings.from_env(db_path="ledger.db"))
service.import_file("examples/sample_ledger.csv")
service.import_lease("examples/sample_lease.pdf")
report = service.lease_report()
for flag in report.flags:
print("•", flag)
How lease cross-examination works
Lease PDFs are unstructured and vary wildly, so LedgerMCP splits the problem:
- Extraction is heuristic but honest. Regex/keyword rules pull the commonfields; each extracted value carries a
Confidence(high/medium/low)and the verbatimexcerptit came from. Whatever isn't matched is simplyleft unset. - The full lease text is retained, so a client can read anything theheuristics miss via
get_lease_text(pet policies, subletting, maintenance). - The comparisons are deterministic. Every check below is exact Decimalarithmetic over stored data — the terms may be fuzzy, but the math isn't:
| Check | What it does |
|---|---|
| Rent | Sums each month's rent charges and compares to the lease's monthly rent (match / over / under / missing). |
| Charge audit | Matches each charge category to base rent, the deposit, a named lease fee, or a mention in the lease text; anything else is unexpected. Flags amount mismatches. |
| Deposit | Compares the deposit charged in the ledger to the lease deposit. |
| Late fees | Compares late fees charged to the lease late-fee amount and grace period. |
Notes:
- National Apartment Association (NAA) leases — the most common USapartment lease — are handled by a dedicated extractor. Signed NAA packetsare flattened forms whose fill-in values are detached from their labels inthe text, so LedgerMCP reads the reliably-anchored fields (base monthly rent,landlord, tenants, address, effective date) and leaves the positionallyambiguous ones (deposit, late fee) for
get_lease_textrather than guessing.- Scanned/image-only PDFs have no text layer; OCR is out of scope and suchfiles are reported with a clear error. Export a text-based PDF if you can.
- For unusual leases, lean on
get_lease_textand the confidence levels.
The ledger data model
Entry types and the sign convention
Every source line normalizes to one EntryType, signed by a single rule:
EntryType |
Meaning | Sign on balance |
|---|---|---|
charge |
Billed to you | + (increases what you owe) |
payment |
You paid | − |
credit |
Concession/discount in your favour | − |
refund |
Money returned to you | − |
adjustment |
Manual correction | + |
A positive ledger balance means money is owed.
Classification (chart of accounts)
The default ClassificationRuleset reflects a renter's perspective: charges →one expense account per category, concessions → income, payments/refunds→ a liability (Accounts Payable). Override per category — e.g. treat asecurity deposit as an asset:
from ledger_mcp import ClassificationRuleset, AccountType
ruleset = ClassificationRuleset(category_overrides={"security deposit": AccountType.ASSET})
Architecture
Layers are decoupled so each is independently testable and swappable:
ledger files ─▶ parsers ─▶ RawEntry ─▶ validate + normalize + classify ─▶ Transaction ─┐
▼
lease PDF ─────▶ lease.pdf ─▶ lease.extract ─▶ LeaseTerms ─────────────────▶ SQLite (SQLAlchemy)
│
┌────────────────────────────────────────────────────────────── ┘
▼
analytics + lease.review (cross-examination)
│
▼
LedgerService ← shared by the MCP server & CLI
| Module | Responsibility |
|---|---|
parsers/ |
Read ledger formats → RawEntry. Registry auto-selects by file type. |
models.py |
Canonical Pydantic models, enums, sign convention. |
classification.py / normalize.py |
Map entries onto a chart of accounts and apply signs. |
validation.py |
Semantic checks + reconciliation against reported balances. |
ingest.py |
The parse → validate → normalize → persist pipeline. |
db/ |
SQLAlchemy schema, engine, repositories. |
lease/ |
PDF text extraction, heuristic term extraction, storage, and cross-examination. |
analytics/ |
Deterministic Decimal queries and statements. |
service.py |
Application facade owning session lifecycle. |
mcp/ |
FastMCP server exposing the tools. cli.py |
Adding a ledger parser
from pathlib import Path
from ledger_mcp.models import RawEntry
from ledger_mcp.parsers import ParseResult, default_registry
class QifParser:
name = "qif"
def can_parse(self, path: Path) -> bool:
return path.suffix.lower() == ".qif"
def parse(self, path: Path) -> ParseResult:
entries: list[RawEntry] = ... # read the file
return ParseResult(entries=entries, source_name=path.name, parser=self.name)
default_registry.register(QifParser())
Validation, classification, storage, analytics and lease cross-examination areall format-agnostic, so nothing else changes.
Development
uv sync
uv run pytest # tests
uv run ruff check . # lint
uv run ruff format . # format
uv run pyright # type check
CI (.github/workflows/ci.yml) runs lint, type-check and tests on every push and PR.
Configuration
| Variable | Meaning | Default |
|---|---|---|
LEDGER_MCP_DB |
SQLite path or SQLAlchemy URL | ./ledger.db |
The CLI --db flag overrides the environment.
Privacy
Everything runs locally. Your ledger and lease live in a local SQLite file; nodata leaves your machine. The bundled examples/ are synthetic.
License
MIT — see LICENSE.