Skip to main content
Glama
themusashimaru

ledgerkit-mcp

README.md
# ledgerkit-mcp

![CI](https://github.com/themusashimaru/ledgerkit-mcp/actions/workflows/ci.yml/badge.svg)

An MCP server that gives AI agents a double-entry ledger they cannot unbalance. Built on [ledgerkit](https://github.com/themusashimaru/ledgerkit).

Agents are increasingly asked to touch money: record a sale, apply a refund, split a commission, reconcile a day. The failure mode is never that the model can't format a journal entry. It's that agents retry, and retries double-post; that models do decimal arithmetic in their heads, and drift; that "fix the balance" is one hallucinated tool call away from rewriting history. This server is a case study in designing tools for that caller: the invariants live below the tool surface, where no prompt can reach them.

## What the agent gets

| Tool | What it does |
|---|---|
| `open_account` | Open an account (asset, liability, equity, income, expense) with an explicit overdraft policy |
| `post_entry` | Post a balanced entry: debits must equal credits, `idempotency_key` required |
| `get_balance` | Current or point-in-time balance of one account |
| `list_accounts` | Every account with type, policy, and balance |
| `list_entries` | The journal, newest first, paginated with a cursor |
| `trial_balance` | Every balance plus proof the books balance |
| `allocate` | Split an amount by ratios without losing a penny |

## What the agent cannot do

There is no update, no delete, no "set balance", no unbalanced write. Corrections are reversal entries, the same as a real ledger. The agent cannot break an invariant because no tool exists that could: safety by construction beats safety by prompt.

## Design rules for agent-facing tools

These are the decisions this repo exists to demonstrate.

**1. Idempotency is required, not polite.** Agents retry. Tool calls time out and get reissued, sessions resume, contexts compact and replay. `post_entry` requires an `idempotency_key` tied to the real-world event (order id, webhook event id), so every retry is a safe no-op that returns `replayed: true`. The same key with *different* contents is a loud conflict, never a silent overwrite. This survives server restarts, because the key index is rebuilt from the journal.

**2. Errors are prompts.** A rejected call returns a message written for the model that caused it: which rule was violated, with the numbers (`debits 100.00 != credits 10.00`), so the next attempt can be correct instead of merely different. An agent that gets `"leg amounts must be positive; express direction with the side, not the sign"` fixes itself. An agent that gets `400 Bad Request` flails.

**3. Reads respect the context window.** `list_entries` paginates newest-first with a hard cap and a `before_seq` cursor. "Return the whole journal" stops being a plan around entry #500, and a tool that can flood the caller's context is a tool that degrades the caller.

**4. The model should never do the arithmetic.** `allocate("100.00", [1,1,1])` returns `["33.34", "33.33", "33.33"]`, summing to exactly the original (largest-remainder method). Penny-perfect division is precisely the operation language models get plausibly wrong, so it's a tool, not a mental math exercise.

**5. The journal is the only truth.** Persistence is one append-only JSONL file. On boot, history replays through the same `post()` path as live traffic, so a tampered or damaged journal refuses to load rather than loading wrong. Balances are derived state, recomputable from the journal at any moment, which is also how point-in-time balances work.

## Setup

```bash
git clone https://github.com/themusashimaru/ledgerkit-mcp
cd ledgerkit-mcp && npm install
```

Claude Code:

```bash
claude mcp add ledger \
  --env LEDGER_FILE=$HOME/.ledgerkit/journal.jsonl \
  -- npx tsx /ABSOLUTE/PATH/TO/ledgerkit-mcp/src/server.ts
```

Any MCP host, same shape:

```json
{
  "mcpServers": {
    "ledger": {
      "command": "npx",
      "args": ["tsx", "/ABSOLUTE/PATH/TO/ledgerkit-mcp/src/server.ts"],
      "env": { "LEDGER_FILE": "/Users/you/.ledgerkit/journal.jsonl" }
    }
  }
}
```

Configuration is two environment variables: `LEDGER_CURRENCY` (`USD` default, `EUR`, `JPY`, or `CODE:decimals`) and `LEDGER_FILE` (path to the journal; unset means in-memory, which is fine for a demo and wrong for anything real).

Then ask your agent to keep books:

> "Open cash, revenue, and sales_tax_payable accounts. Record today's sale #1001: $108.75 collected, $100 revenue, $8.75 tax. Then show me the trial balance."

## Tests

```bash
npm test        # 17 tests over the real MCP protocol (in-memory transport)
npm run smoke   # spawns the real stdio server, posts, restarts it, retries
```

The suite calls tools through an actual MCP client, not the handlers directly, because schema validation is half the contract. The smoke test kills the server mid-flow and proves a retried `post_entry` after reboot is a replay, not a double post.

## Relationship to ledgerkit

The engine (`src/engine/`) is vendored from [ledgerkit](https://github.com/themusashimaru/ledgerkit), a zero-dependency double-entry ledger: balanced-by-construction entries, bigint minor-unit money, append-only journal, idempotent posting. This repo is the agent-facing skin around it. The layering is the point: the engine enforces what must be true, the MCP layer decides what a language model should be allowed to ask for and how it should fail.

## License

MIT

TDQS

A4.1/5.0

Scored across 7 tools

Disambiguation5/5

Each tool targets a distinct action or resource: account querying, creation, entry posting, listing, reporting, and allocation. No two tools could be confused for the same operation, even with overlapping concepts like balance and trial balance.

Naming Consistency4/5

The vast majority follow a clear verb_noun pattern (get_, open_, post_, list_). allocate is a single verb, and trial_balance is a noun phrase, but the all-lowercase underscore style and intuitive verbs keep the set predictable.

Tool Count5/5

At 7 tools, the server covers the core ledger lifecycle (accounts, entries, reports) plus a useful allocation utility. It feels appropriately scoped without redundancy or excess.

Completeness4/5

The tool set covers opening/listing accounts, posting/listing entries, querying balances, and producing a trial balance. Minor gaps like fetching a single entry or account details are workable through list endpoints, and the append-only design intentionally omits deletion/editing.

Maintenance

ActivityStale
ResponsivenessNo issues