Skip to main content
Glama
lastro

lastro

by shiborgi

Lastro

Self-hosted, MCP-first financial platform for personal, family, property, project and business finances. Each independent financial scope is a Book.

Lastro is built to be operated by an AI agent over MCP as its primary interface, with a compact web app for review and direct edits. PostgreSQL is the source of truth.

The one idea worth knowing

Lastro separates economic facts from cash movements:

Expense  ≠  Payment
Revenue  ≠  Receipt

An expense is split into settlements — its installments. A payment groups one or more settlements, may cover installments from several different expenses, and has its own due date. A payment's amount is derived from the active settlements it groups; it is never entered directly. The same holds for revenues and receipts.

That distinction is why a general "transactions" table appears nowhere in the schema.

Related MCP server: Payments Agent Gateway (MCP)

Quick start

Requires Bun 1.4+ and Docker.

cp .env.example .env
# set BETTER_AUTH_SECRET — openssl rand -base64 32

docker compose -f docker-compose.release.yml up -d --build
bun install
bun run bootstrap --email you@example.com --password 'a-long-passphrase'

That prints an MCP credential once. Then:

macOS on Apple Silicon can use Apple Container instead of Docker; see docs/install.md.

Connecting an agent

Lastro speaks MCP over Streamable HTTP and stdio. With the credential printed by bun run bootstrap:

{
  "mcpServers": {
    "lastro": {
      "url": "http://127.0.0.1:3002/mcp",
      "headers": { "Authorization": "Bearer <credentialId>.<secret>" }
    }
  }
}

A credential is bound to one Book. Calls name the Book they act on, and a credential issued for a different one is rejected rather than returning an empty result — so an agent cannot reach another agent's ledger even by asking for it. Give each agent its own Book and credential and they stay separated by construction:

bun run bootstrap --email you@example.com --password '...' --book Butler    --agent agent:butler
bun run bootstrap --email you@example.com --password '...' --book Bartender --agent agent:bartender

Send x-book-id as well to assert which Book you expect: a token for a different one is then rejected instead of writing to the wrong ledger.

The agent gets the whole product across 75 tools: catalog CRUD (institutions, accounts, parties, categories, card billing windows, statement descriptors), the expense and revenue cycles, transfers, positions and cash flow, bank statement import with its review and promotion steps, and the daily close — Yooga sales import, product mapping, posting a day, clearing the acquirer's deposits and reading its result. Every write takes a mandatory idempotency key, every destructive tool requires an explicit confirmation: "confirm", and every mutation appends an audit event naming the agent principal and the operator it acts for.

Bank statements

Lastro reads a statement as a plugin per format — C6 card and account, Nubank account — chosen by the storage path <institution>/<kind>/<file>. A plugin only parses. It never decides that a line is an expense, never invents an institution, and never guesses which merchant a line belongs to: it records the wordings it has never seen, without a destination, for a person to map.

Importing the same file twice is a no-op. Every row carries sha256(institution, account, date, descriptor, amount) plus an occurrence number, so a re-import collides while two genuinely identical purchases on one day stay two rows.

Promotion — turning a reviewed row into an expense, a revenue or a transfer — is a separate step with its own permission, and it refuses rather than guesses when a descriptor names no party or no category. Naming another account on a descriptor is the declaration that its lines are transfers, which is why a transfer seen on both statements collapses to one record instead of two.

Architecture

apps/web       Next.js operator interface (Tailwind CSS v4 + shadcn/ui)
apps/api       authenticated HTTP/JSON API, hosts Better Auth
apps/mcp       MCP adapter — stdio and Streamable HTTP

packages/domain        entities and invariants, no framework
packages/application   commands, queries, authorization, ports
packages/db            PostgreSQL schema, migrations, port implementations
packages/contracts     Zod schemas shared by API, MCP and the web app
packages/auth          agent credentials, RBAC, Book membership
packages/config        validated environment configuration
packages/testing       PostgreSQL-backed integration suite

API, MCP and the web app call the same application handlers. Domain rules never live in routes, tools or components. Only packages/db knows SQL.

Persistence is reached through a port split by aggregate (packages/application/src/ports) whose every member is required, so a repository that cannot answer one fails to compile rather than throwing on the first call that needs it.

Details in docs/architecture.md.

Invariants

  • Money is a bigint amount plus an explicit currency; JSON carries amounts as validated decimal strings. No floats, anywhere.

  • Every command and query is scoped to an authenticated bookId.

  • A settlement may only connect records from the same Book and currency.

  • Active settlements may never exceed their expense's total, nor their receipt's. Both caps are database triggers, because a rule the application alone enforces is a rule two concurrent callers can get around.

  • Status is derived from active settlements, never set directly.

  • Confirmed settlements are voided and replaced, never edited in place.

  • Every mutation is idempotent and appends an AuditEvent.

Development

docker compose up -d          # PostgreSQL 16 on :5432
bun install
DATABASE_URL=postgres://lastro:lastro@localhost:5432/lastro bun run db:init  # apply the schema
bun run check                 # typecheck, lint, unit tests, builds
bun run test:integration      # real PostgreSQL, api + mcp, end to end

Changing the schema: edit the right module under packages/db/src/schema/, then run cd packages/db && bun run db:regenerate. That rebuilds the single drizzle/0000_init.sql and appends triggers.sql. There is one migration by design — Lastro ships a database it creates, not one it upgrades.

Contributing

See CONTRIBUTING.md. Security reports go to SECURITY.md.

License

MIT — see LICENSE. UI primitives are copied from shadcn/ui, also MIT; see THIRD_PARTY_NOTICES.md.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Double-entry accounting ledger MCP server for autonomous agents that enables creating accounts, posting journal entries, and generating financial reports.
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      MCP server for double-entry bookkeeping with idempotent writes, immutable entries, and audit trail. Enables trustworthy financial recording and verification through natural language.
      31 npm
      MIT
    • F
      license
      A
      quality
      C
      maintenance
      Enables AI agents to create accounts, record balanced double-entry transactions, query balances, and reconcile accounts against a SQLite-backed ledger through four MCP tools.
      4
      -