EislM0203andClaude Opus 5 439a15264b feat: fin-mcp as a deployable HTTP service, with typed tools and name resolution
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
2026-08-22 21:20:39 +00:00
2026-06-30 08:37:45 +02:00

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.

S
Description
No description provided
Readme
904 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 56.7%
TypeScript 42.5%
Dockerfile 0.3%
JavaScript 0.2%
Mako 0.1%
Other 0.1%