Skip to main content
Glama
luke-nielsen

ledger-mcp

by luke-nielsen

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.

Related MCP server: Estaite Solutions

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:

  1. Extraction is heuristic but honest. Regex/keyword rules pull the common fields; each extracted value carries a Confidence (high/medium/low) and the verbatim excerpt it came from. Whatever isn't matched is simply left unset.

  2. The full lease text is retained, so a client can read anything the heuristics miss via get_lease_text (pet policies, subletting, maintenance).

  3. 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_text rather 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_text and 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.

Install Server
A
license - permissive license
A
quality
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/luke-nielsen/ledger-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server