luke-nielsen

LedgerMCP

Community luke-nielsen
Updated

Ledger-MCP is an open-source Python MCP server that transforms financial ledgers into deterministic AI tools. Import, validate, and query accounting data through structured tool calls instead of relying on LLM context and probabilistic calculations.

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:

  1. Extraction is heuristic but honest. Regex/keyword rules pull the commonfields; each extracted value carries a Confidence (high/medium/low)and the verbatim excerpt it came from. Whatever isn't matched is simplyleft unset.
  2. The full lease text is retained, so a client can read anything theheuristics miss via get_lease_text (pet policies, subletting, maintenance).
  3. 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_text rather 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_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 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.

MCP Server · Populars

MCP Server · New

    ihuzaifashoukat

    x-use

    Browser-native AI agents for X (Twitter): multi-account, MCP-ready, no X API key required.

    Community ihuzaifashoukat
    hongnoul

    hwatu

    Visual verification browser for AI coding agents: daemon-based WebKitGTK, real rendering, ~13ms window spawn

    Community hongnoul
    SeanFDZ

    AgentBridge

    Let AI agents control Classic Mac OS through a shared folder protocol.

    Community SeanFDZ
    livecontext-ai

    LiveContext

    The AI automation platform, self-hosted. Describe the job in chat and LiveContext builds it: readable workflows, scoped AI agents, and small apps your team uses. Chat, Workflow, Agent and App on one canvas.

    Community livecontext-ai
    timescale

    rsigma

    A complete Sigma detection engineering toolkit: parser, linter, evaluator, correlation engine, conversion framework, streaming daemon, MCP and LSP servers :crab:

    Community timescale