Caudal

Caudal

Finance OS for bootstrapped SaaS, providing MCP tools for transaction recording, projections, alerts, and more through natural language interfaces.

Category
Visit Server

README

Caudal

CI Python 3.12 License: MIT

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_transaction tries MCP elicitation for missing/invalid fields before falling back to a structured clarification_needed result. Try it without Hermes via uv 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 (see planning/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 calling get_digest/check_alerts is 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.yaml into its data volume on first launch — scripts/patch_hermes_config.py (run automatically by make chat) merges this repo's provider/MCP config into it.
  • A freshly-registered MCP server's tools aren't auto-enabled for the cli platform — also handled by make chat via hermes 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

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured