ledger-mcp
Enables renters to cross-examine their rent ledger against their lease using deterministic tools, uncovering discrepancies in charges, fees, and rent amounts.
README
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 server built for renters. Import your property/rent-portal ledger (CSV, TSV, Excel) and your lease (PDF), and LedgerMCP gives an AI assistant a set of deterministic 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 a pasted statement and you get plausible, confidently-wrong numbers. LedgerMCP moves 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 — and shows 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 confidence level and the source excerpt. The full lease text is retained so the assistant 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-portal export 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, income statement, 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-fee discrepancy (lease says $75, ledger charged $50), and coverage gaps (a deposit and 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 to
claude_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 calls
lease_ledger_report, relays the flags, and can drill in with check_rent_charges
or 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 common
fields; each extracted value carries a
Confidence(high/medium/low) and the verbatimexcerptit came from. Whatever isn't matched is simply left unset. - The full lease text is retained, so a client can read anything the
heuristics miss via
get_lease_text(pet policies, subletting, maintenance). - The comparisons are deterministic. Every check below is exact Decimal arithmetic 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 US apartment lease — are handled by a dedicated extractor. Signed NAA packets are flattened forms whose fill-in values are detached from their labels in the text, so LedgerMCP reads the reliably-anchored fields (base monthly rent, landlord, tenants, address, effective date) and leaves the positionally ambiguous ones (deposit, late fee) for
get_lease_textrather than guessing.- Scanned/image-only PDFs have no text layer; OCR is out of scope and such files 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 a
security 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 are all 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; no
data leaves your machine. The bundled examples/ are synthetic.
License
MIT — see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.