Beanie Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Beanie Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
An MCP server that lets AI agents inspect and query Beancount v3 ledgers with BQL. Point it at your .bean file, connect it to any MCP-capable client, and ask questions about your finances while the server handles ledger loading, validation, and structured query results.
Once connected, your agent can query your ledger directly without you touching a terminal. Under the hood, the run_query tool accepts BQL, not arbitrary natural language, so the agent translates your question into a valid query:
"What did I spend on restaurants last month?" "What's my current net worth across all accounts?" "Are there any errors or failed balance assertions in my ledger?" "Show me all transactions in my brokerage account this tax year."
beanie-mcp exposes ten tools and five resources:
| Name | Type | Description |
|---|---|---|
run_query | Tool | Run a BQL query. Returns structured JSON with columns, stringified rows, returned row count, and truncation metadata. Supports an offset param to page past the 200-row cap. Invalid BQL or broken ledgers return an error field instead. |
bean_check | Tool | Run bean-check on the ledger. Returns structured { ok, message, errors } JSON. |
list_accounts | Tool | Return all declared accounts as structured JSON, without the query row cap. |
list_tables | Tool | Return beanquery table names plus the key FROM caveat. |
list_prices | Tool | Latest known price for every commodity with price history, without the 200-row cap. |
find_unmatched_transfers | Tool | Greedy-match postings to a staging/suspense account (opposite sign, equal amount, date within a window) and report unmatched orphans. |
ledger_info | Tool | Orientation facts before querying: operating currency, title, date span, account count/roots, today's date. |
net_worth | Tool | Net worth (Assets + Liabilities) converted to the operating currency. Optional as_of date. |
account_balance | Tool | Balance of an account (and its subtree) converted to the operating currency. Optional as_of date. |
holdings | Tool | Per-commodity units, cost basis, market value, and unrealised gain, converted to the operating currency. |
beanie://accounts | Resource | All accounts in the ledger, one per line. |
beanie://tables | Resource | BQL-accessible table names. |
beanie://prices | Resource | JSON-encoded list_prices() output. |
beanie://bql-guide | Resource | Short BQL guide for agents, including caveats and examples. |
beanie://context | Resource | JSON-encoded ledger_info() output. |
BQL is SQL-like, but it is not SQL. The most important caveat: FROM is a date/filter clause, not a table selector. A query like this does not list accounts from the accounts table:
SELECT account FROM accounts ORDER BY accountFor account discovery, use the list_accounts tool or beanie://accounts resource instead. For table discovery, use list_tables or beanie://tables.
Income, Liabilities, and Equity accounts are credit-normal in Beancount. A row-level query shows each posting's stored sign correctly, but sum(position) aggregates on these account types can read as inverted from what you'd expect — cross-check with a row-level query if an aggregate sign looks surprising.
Useful query examples:
SELECT account, sum(position)
WHERE account ~ "Expenses"
GROUP BY account
ORDER BY accountSELECT date, payee, narration, account, position
WHERE account ~ "Expenses:Food"
LIMIT 50SELECT account, sum(position)
WHERE account ~ "Assets|Liabilities"
GROUP BY accountBQL has no self-join, so matching the two legs of a transfer routed through a staging/suspense account (e.g. Equity:Transfers:Pending) can't be expressed as a query. find_unmatched_transfers does the greedy matching instead: it pairs postings by opposite sign, equal amount, same currency, and date within window_days, and reports whatever's left over as orphans.
find_unmatched_transfers(account="Equity:Transfers:Pending", window_days=2){
"matched_count": 41,
"orphans": [
{"date": "2024-03-02", "amount": "150.00", "currency": "USD", "narration": "..."}
],
"orphan_count": 1
}run_query returns one of three shapes.
Successful query:
{
"columns": ["account", "sum_position"],
"rows": [["Expenses:Food", "123.45 USD"]],
"truncated": false,
"returned_rows": 1,
"offset": 0,
"total_rows": 1,
"total_rows_known": true
}Broken ledger:
{
"error": "Ledger has bean-check errors; fix them before querying.",
"error_type": "ledger",
"errors": [
{
"file": "/path/to/main.bean",
"line": 42,
"type": "BalanceError",
"message": "Balance failed for ..."
}
]
}Invalid BQL:
{
"error": "syntax error or beanquery error message",
"error_type": "bql"
}Rows are capped at 200. To keep broad queries from materialising an entire ledger, beanie-mcp fetches at most 201 rows. When truncated is true, total_rows is null and total_rows_known is false; add a narrower WHERE, ORDER BY, or LIMIT clause if you need a smaller answer — or page through the full result with offset: call again with offset set to the sum of returned_rows seen so far until truncated comes back false. BQL itself has no OFFSET keyword, so this is handled server-side. Row values are returned as strings so MCP clients get stable JSON even when beanquery returns Python dates, decimals, inventories, or other typed Beancount values.
bean_check returns:
{
"ok": true,
"message": "Ledger is clean - no errors or warnings.",
"errors": []
}or:
{
"ok": false,
"message": "Ledger has 1 error(s).",
"errors": [
{
"file": "/path/to/main.bean",
"line": 42,
"type": "BalanceError",
"message": "Balance failed for ..."
}
]
}beanie-mcp is designed for real-world ledgers, not just small demos:
BQLShell text tables to the beanquery DB-API (beanquery.connect()). Agents get columns and stringified rows they can actually work with, not a text table to parse.run_query refuses to query ledgers with loader errors or failed balance assertions instead of returning plausible empty results.BEANCOUNT_LEDGER env var at startup, so the agent cannot point the server at arbitrary files on your filesystem.run_query parses each statement with beanquery's own parser and executes only SELECT, PRINT, BALANCES and JOURNAL. CREATE TABLE (which can read any file), INSERT and dot-commands are rejected before execution.include file changes, not on every query..bean files are modified, created, moved, or deleted..bean file)Beancount v3 is the supported target. Beancount v2 ledgers may not work; if you are on v2, use a v2-compatible MCP server or query tool.
Clone the repo and install the Python dependencies with uv:
git clone https://github.com/klinikal/beanie-mcp.git
cd beanie-mcp
uv syncFind the absolute path to your main Beancount file. For example:
realpath ~/finance/main.beanUse that full path as BEANCOUNT_LEDGER in the MCP config below. Relative paths are deliberately avoided because MCP clients may start the server from a different working directory.
You can smoke-test the server before adding it to an MCP client:
BEANCOUNT_LEDGER=/absolute/path/to/your/ledger/main.bean uv run beanie-mcpThe command starts an MCP stdio server and waits for a client. Press Ctrl+C to stop it.
beanie-mcp is not tied to a particular model or agent. It is a standard local MCP stdio server. Any client that can launch a local MCP command with environment variables should be able to use it. That includes Claude Code/Desktop-style configs, Codex-style agent runners, Cursor-style IDE agents, Gemini-based agents, and other MCP-compatible tools. The exact config UI or file format depends on the client.
The generic command is:
{
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/beanie-mcp",
"beanie-mcp"
],
"env": {
"BEANCOUNT_LEDGER": "/absolute/path/to/your/ledger/main.bean"
}
}Some clients wrap that command in an mcpServers object. Others have a GUI where you enter the same command, args, and env vars separately.
For clients that use an mcpServers JSON block, add:
{
"mcpServers": {
"beanie": {
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/beanie-mcp",
"beanie-mcp"
],
"env": {
"BEANCOUNT_LEDGER": "/absolute/path/to/your/ledger/main.bean"
}
}
}
}Add a server entry to your Codex config:
[mcp_servers.beanie]
command = "uv"
args = [
"run",
"--directory",
"/absolute/path/to/beanie-mcp",
"beanie-mcp",
]
[mcp_servers.beanie.env]
BEANCOUNT_LEDGER = "/absolute/path/to/your/ledger/main.bean"Use the same command, args, and env vars wherever your client defines local MCP servers:
{
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/beanie-mcp",
"beanie-mcp"
],
"env": {
"BEANCOUNT_LEDGER": "/absolute/path/to/your/ledger/main.bean"
}
}Restart the client after changing MCP config. MCP clients usually read the tool list only when they start.
Once connected, ask your agent to run:
bean_checklist_accountslist_tablesThen try a small BQL query:
SELECT account, sum(position)
WHERE account ~ "Expenses"
GROUP BY account
LIMIT 20If bean_check reports errors, fix the ledger first. run_query refuses to query a broken ledger so the agent does not mistake an invalid ledger for an empty result.
To update an existing local checkout:
cd /absolute/path/to/beanie-mcp
git pull
uv syncRestart your MCP client after updating.
The client cannot find `uv` Use the absolute path to uv in your config. Find it with:
which uvThen replace "command": "uv" with something like "command": "/opt/homebrew/bin/uv".
`No ledger configured` Set BEANCOUNT_LEDGER in the MCP config env block. It must point to your main .bean file.
Ledger file not found Use absolute paths for both /absolute/path/to/beanie-mcp and BEANCOUNT_LEDGER. ~ may not expand inside every MCP client.
Tool list did not change after updating Restart the client. Long-running MCP clients often keep the old tool list until they reconnect.
Run the MCP inspector:
BEANCOUNT_LEDGER=/absolute/path/to/your/ledger/main.bean uv run mcp dev src/beanie_mcp/server.pyuv run pytest server_test.py -v
uv run ruff check .
uv buildThis tool sends parts of your Beancount ledger to whatever model/provider your MCP client uses. Only connect it to a provider and client you trust with your financial data. The relevant data handling policy is the one for the model/provider/client you choose.
You are responsible for your financial data. Don't connect this to a service you wouldn't trust with your bank statements. Run this at your own risk.
MIT. See LICENSE.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.