Makes the MCP server runnable as a network service and hardens its tool surface. Previously it was stdio-only and had never been executed — the test file had never run, and 3 of its 5 tests failed on first invocation. Transport and deployment FIN_MCP_TRANSPORT selects stdio (default, unchanged) or streamable-http, which binds FIN_MCP_HOST:FIN_MCP_PORT and serves MCP at FIN_MCP_PATH (default /mcp) with stateless_http so no session affinity is needed. Ships as its own image (cli/Dockerfile) and its own Deployment + ClusterIP Service (deploy/k8s/fin-mcp.yaml) rather than being mounted into the API app — matching how the other MCP servers in the cluster are deployed, and keeping the mcp dependency out of the api image. The manifest is a reference to copy into the Flux repo; it is not applied from here. The manifest deliberately has no HTTPRoute. fin-mcp has no auth of its own and the API runs with FIN_API_TOKEN unset, so with writes enabled the only thing keeping it safe is being unreachable from outside the cluster. Correctness create_transaction accepted a free-form type string while POST /api/transactions accepts all six transaction types. A caller passing type=transfer would write a single leg with no linked_account_id — an orphan that corrupts balances and that paired-delete can never clean up. All eight enum-valued parameters are now Literal-typed, so the client rejects such calls before they are made. TestInvariantGuards locks this down. FinClient was constructed eagerly in create_server(), so a missing FIN_API_URL killed the process before the MCP handshake and surfaced to the client only as "server failed to start". It is now built lazily on first use, turning the same misconfiguration into a readable error. Tool surface Adds find_account / find_category, which map a name to the integer id every other tool requires — exact match wins, otherwise all substring matches are returned so the caller can disambiguate. Publishes accounts and categories as the resources fin://accounts and fin://categories. Drops get_openapi_spec: the tools already cover the API and the spec is too large to justify a tool slot. The three update_* tools took an untyped dict[str, Any], which produced a schema with no properties at all; they now take explicit optional parameters, plus clear_* flags so the ability to null a field survives the change. Removes ~48 lines of write-warning boilerplate duplicated across 24 tools — it lives in the server instructions once, and no longer claims refresh_prices can double-book. Fixes fetch_ticker_price claiming it blocks for minutes when the client times out at 30s, and deactivate_account flagging itself destructive while documenting itself as reversible. Tests: 37 passing, up from 27 passing / 3 failing. The failures were harness bugs — mcp >=1.12 returns (content, structured) from call_tool where the helper assumed a list. Verified against a live API over both transports: handshake, reads, writes, name resolution, resource reads, and rejection of the transfer type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L3qYLuu9jgXsCRXmJ18YGY
fin
A self-hosted, single-user personal net worth tracker. fin aggregates bank accounts, investment portfolios, Splitwise balances, and receivables into a single EUR-denominated view, with daily automated snapshots and live stock prices.
Features
- Dashboard — current net worth broken down by account group, plus recent transactions
- Spending — monthly expense breakdown by category with a pie chart
- Portfolio — investment account holdings with live USD prices (via yfinance), P&L per position, and EUR conversion
- History — net worth over time as a line chart; snapshots taken automatically at midnight
- Transactions — full transaction log across all accounts
- Quick-add flows — mobile-friendly sheet for logging expenses, transfers, Splitwise splits/settlements, and stock trades
- Settings — manage accounts (bank, investment, RSU, Splitwise, receivable) and spending categories; manual balance override per account
- Multi-currency — EUR base currency; exchange rates via Frankfurter API
Quick Start
git clone <repo>
cd fin
docker compose up -d
The app is available at http://localhost.
Environment / Config
Set in docker-compose.yml under the api service:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:////data/fin.db |
SQLite path inside the container |
FIN_API_TOKEN |
(unset — open) | Bearer token the API requires; leave unset for no auth |
CLI / MCP environment variables (set in the shell or MCP client config):
| Variable | Default | Description |
|---|---|---|
FIN_API_URL |
(required) | Base URL of the fin API, e.g. https://fin.example.com |
FIN_API_TOKEN |
(unset) | Bearer token for the API (only if the API enforces auth) |
FIN_MCP_ALLOW_WRITES |
(unset — read-only) | Set to 1 to enable the 24 write tools in the MCP server |
FIN_MCP_TRANSPORT |
stdio |
stdio, or streamable-http to run as a network service |
FIN_MCP_HOST / FIN_MCP_PORT |
0.0.0.0 / 8000 |
Bind address, streamable-http only |
FIN_MCP_PATH |
/mcp |
Path MCP is served at, streamable-http only |
Data is persisted in a named Docker volume (sqlite_data).
MCP server
fin-mcp exposes the full fin API as structured tools. Default mode is
read-only (15 tools). Set FIN_MCP_ALLOW_WRITES=1 to additionally enable
24 write tools (create/update/delete transactions, accounts, stock trades, etc.).
Enum-valued arguments are typed, so the client rejects invalid values before the
call. create_transaction deliberately accepts only expense and income:
two-leg writes must go through create_transfer, create_settlement,
create_split_expense or create_splitwise_i_paid, or the ledger ends up with
an orphan leg.
Use find_account / find_category to turn a name into the integer id the
other tools take. Both lists are also published as the resources
fin://accounts and fin://categories.
Transports. stdio (default) suits clients that spawn the process.
streamable-http runs it as a service — it binds FIN_MCP_HOST:FIN_MCP_PORT
and serves MCP at FIN_MCP_PATH:
FIN_API_URL=http://localhost:8000 FIN_MCP_TRANSPORT=streamable-http fin-mcp
In Kubernetes it runs as its own Deployment and ClusterIP Service — see
deploy/k8s/fin-mcp.yaml. That manifest has no HTTPRoute on purpose: the MCP
server has no auth of its own, so with writes enabled it must stay unreachable
from outside the cluster unless FIN_API_TOKEN is set on both the API and the
MCP server.
Install:
pip install -e 'cli/[mcp]'
Run manually:
export FIN_API_URL=https://fin.example.com
fin-mcp # stdio; connect via your MCP client
Claude Desktop / Claude Code config (~/.claude/mcp.json or equivalent):
{
"mcpServers": {
"fin": {
"command": "fin-mcp",
"env": {
"FIN_API_URL": "https://fin.example.com",
"FIN_API_TOKEN": "...",
"FIN_MCP_ALLOW_WRITES": "0"
}
}
}
}
Remove FIN_API_TOKEN if the API runs without auth. Change FIN_MCP_ALLOW_WRITES to "1"
to permit write operations (confirm amounts, accounts, and dates with the user before each call).
Architecture
The backend is a Python FastAPI app backed by SQLite via SQLAlchemy; APScheduler runs a nightly job to fetch stock prices (yfinance) and currency rates (Frankfurter) and record a net worth snapshot. The frontend is a React + TypeScript SPA served by Nginx, communicating with the API through the same Nginx container via reverse proxy.