Fungible is a terminal-based personal finance app that exposes its double-entry ledger to MCP-compatible agents without running a web server or syncing to the cloud. The entire stack lives on your filesystem. The interesting constraint: how do you let an AI agent query balances, categorize transactions, and generate reports while preventing it from corrupting accounting state?
The answer involves treating the ledger as an append-only log, exposing read-only views through MCP tools, and using filesystem locks for write coordination. No HTTP endpoints. No database server. Just structured files and a protocol boundary.
Architecture: Filesystem as State Store
Fungible stores all financial data in a local directory structure:
ledger.json: Append-only transaction log (double-entry format)accounts.json: Chart of accounts with metadatarules.json: Categorization rules for transaction matchingimports/: CSV files from bank downloads or Plaid sync
The MCP server runs as a separate process that reads these files. It never writes directly to the ledger. Instead, it exposes tools that return structured data or generate proposed transactions for human approval.
MCP Tool Boundaries
The MCP integration defines four tool categories:
Read-only queries:
get_balance: Returns current balance for an accountlist_transactions: Filters transactions by date, account, or categoryget_net_worth: Calculates assets minus liabilities
Analysis tools:
categorize_transactions: Suggests categories for uncategorized entriesgenerate_budget_report: Aggregates spending by category and time periodfind_duplicates: Detects potential duplicate transactions
Proposal generators:
propose_transaction: Returns a JSON transaction object for reviewsuggest_rule: Generates a categorization rule based on transaction patterns
Import helpers:
parse_csv: Converts bank CSV to ledger format (does not write)
The key design choice: no tool directly modifies the ledger. Every write operation returns a proposal that the user must approve in the terminal UI.
Double-Entry Integrity Without Database Constraints
Personal finance apps need to maintain double-entry bookkeeping rules:
- Every transaction has equal debits and credits
- Account balances must reconcile
- Historical transactions are immutable
Fungible enforces these rules at the application layer, not in a database. The ledger file is append-only. Corrections happen by adding reversing entries, not by editing existing records.
When an MCP agent proposes a transaction, the terminal UI validates it before appending:
def validate_transaction(txn):
# Check double-entry balance
debits = sum(entry['amount'] for entry in txn['entries'] if entry['type'] == 'debit')
credits = sum(entry['amount'] for entry in txn['entries'] if entry['type'] == 'credit')
if debits != credits:
raise ValidationError(f"Unbalanced transaction: {debits} != {credits}")
# Check account existence
for entry in txn['entries']:
if entry['account'] not in accounts:
raise ValidationError(f"Unknown account: {entry['account']}")
# Append to ledger
with FileLock('ledger.lock'):
ledger = read_ledger()
ledger['transactions'].append(txn)
write_ledger(ledger)
The filesystem lock prevents concurrent writes from the terminal UI and any background import processes. The MCP server never acquires this lock because it never writes.
Concurrent Access Pattern
The terminal UI and MCP server can run simultaneously. The user might be reviewing transactions in the TUI while an agent analyzes spending patterns. This creates a read-write coordination problem.
Fungible solves it with optimistic reads and write serialization:
- MCP server: Reads ledger files without locks. If data changes between reads, the worst case is stale analysis. The user will see fresh data in the UI.
- Terminal UI: Acquires a write lock only when appending transactions. Reads are lock-free.
- Import processes: Acquire the same write lock when adding bulk transactions from CSV or Plaid sync.
This works because financial data has natural append-only semantics. You never delete a transaction. You add a correction. The ledger grows monotonically.
MCP Integration Without a Server Process
Most MCP integrations assume a long-running server. Fungible takes a different approach: the MCP server is a CLI tool that responds to a single request and exits.
# Agent calls this for each tool invocation
fungible mcp-tool get_balance --account "checking"
The MCP client (Claude Desktop, Cline, or another agent framework) spawns the process, reads the JSON response, and terminates it. No persistent connection. No state held in memory between requests.
This design has trade-offs:
| Approach | Latency | Memory | Complexity |
|---|---|---|---|
| Long-running server | Low (in-memory cache) | High (persistent process) | High (connection management) |
| CLI per request | Medium (file I/O each time) | Low (process exits) | Low (stateless) |
For a personal finance app, the CLI approach works. Ledger files are small (under 1MB for years of transactions). Reading from disk takes milliseconds. The user is not waiting for sub-second responses.
Plaid Sync in a Local-First World
Fungible supports Plaid for automatic bank transaction sync, but it does not send your ledger to the cloud. Instead:
- Plaid API calls happen from your machine
- Transactions download to
imports/plaid/ - The terminal UI shows a review screen
- You approve or reject each transaction
- Approved transactions append to the local ledger
The MCP server can read from imports/plaid/ to help categorize new transactions based on past patterns, but it cannot trigger a Plaid sync. That requires explicit user action in the terminal UI.
This keeps your financial data local while still allowing automated import. The Plaid access token lives in a local config file, not on a remote server.
Observability: Logs and Audit Trail
Every MCP tool call logs to ~/.fungible/mcp.log:
2026-05-26T10:15:32Z [get_balance] account=checking result=2847.32
2026-05-26T10:15:45Z [categorize_transactions] count=12 suggestions=8
2026-05-26T10:16:01Z [propose_transaction] amount=45.00 category=groceries status=pending
The ledger itself acts as an audit trail. Each transaction includes:
- Timestamp
- Source (manual, CSV import, Plaid sync, MCP proposal)
- User approval status
If an agent proposes a bad transaction, you can trace it back through the logs and see exactly what tool call generated it.
Failure Modes
Corrupted ledger file: The append-only design limits damage. If the JSON file becomes malformed, you lose only the last transaction. The terminal UI validates JSON on every write. A backup script can snapshot the ledger daily.
Agent proposes unbalanced transaction: The validation layer rejects it before it touches the ledger. The agent sees an error response and can retry with a corrected proposal.
Concurrent write collision: The filesystem lock serializes writes. If the terminal UI and an import process both try to write, one blocks until the other finishes. No lost transactions.
Plaid token expires:
The terminal UI shows an error and prompts for re-authentication. The MCP server cannot trigger this flow. It will see stale data in imports/plaid/ until the user manually refreshes.
Agent hallucinates account names: The validation layer checks account existence. If an agent references “credit_card” but the chart of accounts only has “credit-card-visa”, the transaction is rejected.
Technical Verdict
Use Fungible’s approach when:
- You want full control over your financial data with no cloud dependency
- You prefer terminal UIs and can tolerate manual approval steps
- You need agent assistance for categorization and reporting, not autonomous trading
- Your transaction volume is low enough that file I/O latency is acceptable (under 10,000 transactions)
Avoid this pattern when:
- You need real-time multi-device sync (the filesystem is the source of truth)
- You want agents to execute transactions without human approval
- You have high transaction volume that would make file parsing slow
- You need complex queries that would benefit from SQL indexes
The local-first MCP integration works because personal finance has natural boundaries. Transactions are append-only. Balances derive from history. Agents assist but do not decide. The filesystem provides enough coordination for a single-user app.