Architecture
avenir-mcp is a Python package built on FastMCP. Its design keeps three things apart: what talks to YNAB, what computes, and what the agent sees.
Modules
Section titled “Modules”Directoryavenir_mcp/
- server.py — entry point: transport, log level, read-only policy
- app.py — the FastMCP instance, the
writetag, month validation, log filter - tools_budget.py — read tools close to YNAB, and
approve_transactions - tools_classify.py —
suggest_categories,apply_categories - tools_accounts.py —
reconcile_account,forecast_balance,create_transactions - tools_categories.py —
create_category,update_category,set_category_budget - tools_undo.py —
undo_operation, for every kind of operation - context.py — resources and prompts
- confirm.py — preview, confirmation, apply, journal
- client.py — the only code that talks to YNAB
- journal.py — the only code that writes to disk
- triage.py — pending transactions and suggestions, pure
- classifier.py — payee normalisation and scoring, pure
- writes.py — plans, undo plans, confirmation codes, pure
- reconcile.py — account analysis, pure
- forecast.py — recurring charges and projection, pure
- analytics.py — month overview, balances, trends, pure
| Layer | Modules | Rule |
|---|---|---|
| Tools | tools_*.py, context.py |
declare what the agent sees; orchestrate; never compute |
| Confirmation | confirm.py |
the single path every confirmed write goes through |
| Logic | triage, classifier, writes, reconcile, forecast, analytics |
pure functions: no network, no disk, fully unit-tested |
| I/O | client.py (HTTP), journal.py (disk) |
the only side effects |
Amounts cross the I/O boundary in YNAB’s milliunits and leave the logic in currency units: no tool ever returns milliunits.
A read call
Section titled “A read call”- The client calls
get_monthly_summarywith{"plan_id": "last-used", "month": "2026-09-01"}. - FastMCP validates the arguments against the tool’s input schema.
- The tool checks
month(app.check_month) before any request: a malformed month is refused with a message, at no cost. client.get_monthsends oneGET /plans/last-used/months/2026-09-01to YNAB.analytics.month_overviewkeeps the totals and the overspent categories, in currency units.- FastMCP validates the answer against the output schema and returns it as
structuredContent, with its JSON text for older clients.
A write call
Section titled “A write call”- The client calls
apply_categorieswith assignments. - The tool reads the current state (
client.get_transactions,client.get_categories) and askswrites.plan_categorizationwhat would change. Invalid assignments are refused here, before anything is asked. confirm.write_planasks the user through the channel the client supports — see Confirmation. Until the answer is yes, it returns the preview.- On yes, one bulk
PATCH /plans/{id}/transactionsapplies every change. journal.Journal.recordappends the operation to the journal; its id comes back to the agent for undo.
Registration and read-only mode
Section titled “Registration and read-only mode”Every tool is registered at import time with MCP annotations (readOnlyHint,
destructiveHint, idempotentHint, openWorldHint); write tools also carry the tag
write. At start-up, server.main calls app.configure(enable_writes=...), which
disables every tool tagged write unless AVENIR_MCP_WRITE=1. A disabled tool is
neither listed nor callable. A test fails if a tool lacks annotations or if its tag and
its readOnlyHint disagree.
Dependencies
Section titled “Dependencies”| Package | Why |
|---|---|
fastmcp |
the MCP server: protocol, schemas, transports |
httpx |
HTTP requests to YNAB |
pydantic |
already required by fastmcp; turns field docstrings into schema descriptions |
Nothing else at run time. Adding a dependency needs a written reason (AGENTS.md).
Unofficial project. We are not affiliated, associated, or in any way officially connected with YNAB or any of its subsidiaries or affiliates. YNAB and You Need A Budget are registered trademarks of YNAB. avenir-mcp is provided as is, without warranty, and is not financial advice. Legal notice