Caudal
Finance OS for bootstrapped SaaS, providing MCP tools for transaction recording, projections, alerts, and more through natural language interfaces.
README
Caudal
Ask, don't guess.
Caudal (Spanish: both water flow and wealth) — the finance OS for a bootstrapped SaaS.
Excel wasn't going to hold up as the finances of the business grew, and a nicer spreadsheet wasn't the fix — that's just the same manual entry with better colors. Caudal was built from day one as something else: a navigation map for Vertex and whatever it ships next, not a chore that demands data entry as the price of admission. So it lives inside the tools already in use — Hermes Agent, Vertex's day-to-day operating agent, for quick capture and checks, and Claude as the interface for decisions that go beyond what a daily agent should carry (scenario modeling, quarterly reviews, what's next). Concretely, that's two doors into one source of truth: a server-side web interface, and an MCP server exposing the same capabilities as tools.
What this is
- Money is stored as integer minor units (never floats), every write is idempotent and audit-logged.
- Single-currency ledger by construction: USD amounts are converted to COP at the official daily TRM (Banco de la República, via datos.gov.co) at record time (
core/fx.py) — aggregates never mix currencies. - Beyond recording what happened, the system computes forward-looking projections (cash-flow, runway, MRR) and runs a proactive alert engine (budget overruns, runway thresholds) on a schedule.
- Ships with CI, structured logging/tracing/metrics, backups, and a tested restore path — production hygiene, not a script.
Domain background (SaaS accounting fundamentals, category taxonomy, metric formulas) lives in finanzas-saas.md.
Why MCP, not A2A: Hermes has no Google A2A support but has first-class MCP client support, and MCP's elicitation capability (elicitation/create) lets a tool call pause and ask a clarifying question mid-conversation — exactly the "don't guess, ask" behavior this needs.
Architecture
flowchart LR
subgraph Chat
H[Hermes Agent<br/>Telegram / Slack / CLI]
end
subgraph Caudal
M[MCP server<br/>stdio]
C[core/<br/>validation · repository<br/>projections · alerts]
S[scheduler<br/>proactive digests & alerts]
W[Internal web UI<br/>FastAPI]
end
PG[(Postgres)]
LF[Langfuse Cloud<br/>optional, via OpenRouter Broadcast]
H <--MCP tools--> M
M --> C
S --> C
W --> C
C --> PG
M -. traces .-> LF
W -. traces .-> LF
Both entry points — MCP tools and the internal UI — go through the same core/ validation and storage layer, so a transaction created by chat and one entered by hand follow identical rules.
Tech stack
| Concern | Choice |
|---|---|
| Language / packaging | Python, uv |
| MCP server | official mcp Python SDK, stdio transport |
| Web UI | FastAPI + Jinja2, server-rendered — CSS design system + inline SVG charts, no JS framework, no build step |
| Database | Postgres, SQLAlchemy, Alembic |
| Scheduler | APScheduler (fallback when Hermes cron isn't set up) |
| Logging / tracing / metrics | structlog (JSON), OpenTelemetry, prometheus-client |
| Agent observability & cost | Langfuse Cloud via OpenRouter's native Broadcast + per-key spending cap |
| Lint / types / security | ruff, mypy, bandit, pip-audit, gitleaks |
| Tests | pytest, testcontainers, hypothesis |
| Containers | Docker, Docker Compose |
Repository layout
caudal/
core/ # domain layer: repository, validation, reporting, projections, alerts, fx (TRM), logging/tracing
mcp_server/ # MCP tool definitions (thin wrappers over core/)
web/ # FastAPI app, Jinja templates, static design system, server-side SVG charts
scheduler/ # proactive digest/alert runner (no-Hermes fallback)
config.py # environment-driven settings, fail-fast on missing values
tests/
alembic/ # database migrations
planning/ # roadmap — one doc per phase, statuses tracked there
docker/ # compose profile support files (hermes-dev config, etc.)
.github/workflows/
Dockerfile
docker-compose.yml
How to run
make all # core app + pre-build the caudal venv Hermes needs
make chat # interactive Hermes chat session (set OPENROUTER_API_KEY in .env first)
make help # every target: up / hermes-warm / chat / ps / logs / down / down-all / restore-drill
Or directly with Docker Compose (just the core app):
cp .env.example .env
docker compose up -d
curl http://localhost:8000/healthz
This brings up Postgres, runs migrations, the web UI (:8000), the proactive scheduler, and a daily-backup service (docker/backups/, RETENTION_DAYS default 14). Restore a backup with scripts/restore.sh <dump> or make restore-drill.
Local dev without Docker:
uv sync --all-groups
cp .env.example .env
uv run pytest --cov=caudal --cov-report=term-missing # 85% coverage gate, real Postgres via testcontainers
uv run pre-commit install
CI (.github/workflows/ci.yml) runs lint → type-check → SAST → dependency audit → tests on every push, plus a separate gitleaks job.
Core concepts
core/is the single implementation both MCP tools and the UI call into:validation.py(pure input validation),repository.py(idempotent CRUD + audit log),reporting.py/projections.py(SQL aggregates, deterministic forecast/runway math — no LLM),alerts.py(deduplicated proactive rules).- MCP tools (
caudal/mcp_server/server.py, 8 tools):record_transaction,update_transaction,list_transactions,get_totals,list_categories,get_projections,get_digest,check_alerts.record_transactiontries MCP elicitation for missing/invalid fields before falling back to a structuredclarification_neededresult. Try it without Hermes viauv run mcp dev src/caudal/mcp_server/server.py. - Internal UI (
caudal/web/,uv run uvicorn caudal.web.app:app --reload): dashboard (net cash flow hero, runway meter, expense/infra breakdowns), transaction/budget CRUD, alert history with human-readable payloads, projections (forecast + assumptions), reports (net by month, MoM deltas, category breakdowns with date filters),/healthz,/metrics. Light/dark, mobile bottom-tab navigation. No auth in v1 — single-user, localhost/private-network use only (seeplanning/03-remote-access.md). - Scheduler (
uv run caudal-scheduler): daily alert check + weekly digest, delivered via webhook (NOTIFIER_WEBHOOK_URL) or logged if unset. This is the fallback path — once Hermes is available, its own cron callingget_digest/check_alertsis the primary delivery path (see below).
Connecting to Hermes
On the machine where Hermes actually runs:
hermes mcp add caudal --command "/app/.venv/bin/caudal-mcp"
or the equivalent in config.yaml:
mcp_servers:
caudal:
command: "/app/.venv/bin/caudal-mcp"
env:
DATABASE_URL: "postgresql+psycopg://finance:finance@<host>:5432/finance"
tools:
include:
[record_transaction, update_transaction, list_transactions, get_totals,
list_categories, get_projections, get_digest, check_alerts]
Then set up Hermes cron for proactive delivery: "Every Monday at 9am, call the caudal get_digest tool and post the result to Telegram."
Optional: local Hermes chat (--profile hermes-dev)
docker-compose.hermes-dev.yml runs a local Hermes instance for testing the chat → MCP flow without Telegram/Slack, routed directly to OpenRouter (docker/hermes/config.yaml). Set OPENROUTER_API_KEY in .env; budget cap and LLM tracing are OpenRouter dashboard settings (per-key spending cap, "Broadcast to Langfuse") — see docs/observability.md.
Verified end-to-end: a chat message correctly triggers record_transaction, the elicitation flow corrects an invalid field, and the row lands in Postgres. Two notes if you're touching this profile:
- Hermes writes its own
config.yamlinto its data volume on first launch —scripts/patch_hermes_config.py(run automatically bymake chat) merges this repo's provider/MCP config into it. - A freshly-registered MCP server's tools aren't auto-enabled for the
cliplatform — also handled bymake chatviahermes tools enable.
Engineering log
CHANGELOG.md— what changed, per release (Keep a Changelog).docs/adr/— why it changed: Architecture Decision Records for every decision that shapes the data model, an invariant, or the architecture — including the bugs that earned one (e.g. ADR-0008, a forecast double-count found with real data).planning/— where it's going, one doc per phase.
Status & roadmap
Core platform: complete. Bootstrap, data model, core layer, MCP tools + elicitation, internal UI (redesigned: shadcn-style design system, SVG charts, mobile nav), automatic USD→COP conversion at the daily TRM, proactive scheduler, observability, CI, containerization, Hermes dev integration.
Where it's going next, in plain terms: registering receipts and invoices without typing them in by hand, keeping track of what clients owe and chasing that down automatically, and getting flagged the moment a spend spike or an unexpected charge shows up — before it's just a number buried in next month's report. Full detail per phase in planning/:
| Phase | Scope | Status |
|---|---|---|
| 01 — Revenue | Clients, plans, subscriptions, invoices/AR (cartera), payroll category, real MRR | In progress |
| 02 — AI & automation | Auto-registering invoices from email (with a review queue, never blind), narrated digests, client payment/collections automation, spend-spike and unexpected-charge alerts | Planned |
| 03 — Remote access | Web UI auth, MCP over streamable HTTP, channel roles (Hermes = capture, Claude = analysis) | Planned |
License
MIT — see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.