Skip to content

Development

  • Directoryavenir_mcp/ — the server (see Architecture)
    • …
  • Directorytests/ — unit tests, protocol tests, documentation and hygiene tests
    • …
  • Directoryevals/ — the demo plan, a stand-in for YNAB’s API, the agent evaluation
    • …
  • Directorydocsgen/ — generates the tool reference, the error catalogue and the examples
    • …
  • Directorydocs/ — this site (Astro Starlight, English, French, Spanish, German, Dutch)
    • …
  • AGENTS.md — the contract for contributors, human or not
  • justfile — every command below
  • pyproject.toml, uv.lock — the Python project, locked
Terminal window
git clone https://github.com/mathbeal/avenir-mcp && cd avenir-mcp
uv sync # Python 3.14 and the locked dependencies
just check # every gate CI runs

just check runs, in CI’s order:

Check Command Fails when
format ruff format --check a file is not formatted
lint pylint the score is below 10.00
ruff ruff check a docstring of the package is not in Google style or leaves out an argument, the return value or an exception raised; imports are not sorted; bandit’s security checks find a risky pattern (TLS not verified, a shell command, a password in the code…)
docstrings pydoclint a docstring of the package lists the arguments in another order than the signature, gives a type the annotation already gives, or is a bare summary for a function that takes arguments or returns a value
YNAB API terms pytest -m ynab_terms, python -m docsgen.terms a rule of YNAB’s API terms is broken (naming, attribution word for word, YNAB’s own image, a token for its owner only, the hourly limit), or YNAB changed its terms since the date checked; the README badge shows it
dependencies deptry . the code imports a package pyproject.toml does not declare, or declares one it does not use
types mypy (strict) any type error
tests pytest a test fails, any warning is raised, or coverage of lines or branches is below 100 %
vocabulary lexdrift check avenir_mcp --baseline lexdrift.lock a new word appears for an idea already named
lock uv lock --check uv.lock does not match pyproject.toml

CI also runs lychee on every link of the README and the documentation (just links, and every Monday, since a page elsewhere can disappear), typos, zizmor on the workflows, reuse lint (every file states its copyright and licence), two builds of the wheel and the sdist that must be identical bit for bit (just reproducible), pip-audit on the locked dependencies, gitleaks on every commit of the history (just secrets, with Docker), the documentation build with the tests of its own code, CodeQL and the OpenSSF Scorecard (once the repository is public), a weekly comparison of YNAB’s API with its snapshot (just api-drift), and the official MCP Inspector (just inspect), which checks the server as a client sees it on the demo plan: its lists, the portability of its tool schemas, and one call of each kind. The test suite runs on Linux and macOS with Python 3.12, 3.13 and 3.14, and on Windows with Python 3.14 (on a pull request, macOS tries 3.14 only: its runners cost ten Linux ones); coverage is enforced on Linux and macOS, where the file-permission checks apply. Python 3.15 runs too, as an experiment: a failure there is a warning, not a red check. On a pull request, every commit must be signed off and the title must start with the kind of change. Two checks must be green: CI passed, which stands for every job of the quality workflow, and MCP Inspector, a workflow of its own so that its badge shows it alone.

Renovate opens pull requests every Monday morning:

Update Handling
development tools, CI actions, the documentation site (minor and patch) grouped, merged by itself once CI passes
fastmcp, httpx, pydantic — they run in front of users’ plans one pull request each, reviewed by hand; the evaluation runs before a fastmcp update
any major version one pull request each, reviewed by hand
a known vulnerability at once, whatever the day

A new release waits seven days before Renovate proposes it, so a broken or malicious version withdrawn quickly never reaches the project.

Kind Files What they check
Unit test_classifier, test_triage, test_writes, test_reconcile, test_forecast, test_analytics, test_journal, test_client the pure logic and the HTTP client, with invented data
Properties test_properties with Hypothesis, rules that hold for any input: amounts round-trip, spreading keeps every cent, months add up, cursors, bank text, confirmation fingerprints
Protocol test_protocol*, test_context, test_policy tools through an in-memory MCP client: schemas, annotations, confirmation paths, undo
Documentation test_docs generated pages and examples match the code; every page exists in each translation; every environment variable is documented
API coverage test_api_coverage every operation of YNAB’s API is used by a tool, planned or left out with a reason; the client calls only documented paths
Hygiene test_hygiene no IBAN, token, statement, home path or Finder copy in the repository
Evaluation test_evals the evaluation’s own checks are right

Tests are written first. No test calls YNAB.

just mutate runs mutation testing (mutmut) on the modules that compute: it changes the code on purpose, some 1,500 times, and checks that a test fails each time. The changes no test notices point to the tests to write; many are harmless, such as a message’s wording.

just bench times the modules that compute on a generated five-year plan, about 9,000 transactions (pytest-benchmark). On a laptop the slowest, preparing a page of suggestions, takes about 30 ms: a tool’s time is spent waiting for YNAB, not computing.

just fuzz runs the Hypothesis properties with 50,000 examples each instead of 100 (a few minutes; just fuzz 200000 for longer), on what comes from outside: bank labels, damaged journals, HTTP headers, amounts, cursors. A failure is saved and replayed, minimised, by the next run. CI does it every night with 200,000. The first run found a journal byte that was not text (0x80) surfacing as a bare codec error instead of the line to fix.

Terminal window
uv run python -m docsgen # tool pages, error catalogue, examples
just docs # build the site in every language; broken links fail
just docs-serve # live preview on http://localhost:4321/avenir-mcp/

docsgen runs avenir-mcp on the demo plan and writes what it really answers, with the date fixed and random ids replaced. Hand-written pages exist in English, French, Spanish, German and Dutch; a test fails when one is missing.

The site is published on GitHub Pages by the docs workflow, and on Cloudflare Pages (avenir-mcp.pages.dev), which builds it from the repository with npm run build:cloudflare. Both serve it under /avenir-mcp/; on Cloudflare the root redirects there. GitHub Pages cannot send security headers; Cloudflare Pages sends those of docs/cloudflare/_headers with every file: a Content-Security-Policy, HSTS, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, a referrer policy, a permissions policy and an opener policy. The policy allows the site’s own files and each inline script and style of the built pages by its SHA-256, computed at build time; 'wasm-unsafe-eval' lets the search run its WebAssembly, and style attributes are allowed through style-src-attr only. The build fails, in CI too, when a required header is missing or weakened. The DOCS_SITE environment variable sets the address used in canonical links and the sitemap.

Terminal window
just evaluate # all tasks, Sonnet, about 1 USD of your Claude plan
uv run python -m evals.run --task classify --model haiku

See Evaluation.

  • Commit messages start with a type — feat:, fix:, docs:, refactor:, test:, build:, ci:, chore: — because CHANGELOG.md is generated from them by git-cliff.
  • To release: just changelog, commit, tag vX.Y.Z matching avenir_mcp.__version__, and publish a GitHub release. The publish workflow builds twice with the tagged commit’s date, stops unless both builds agree, runs twine check --strict and uploads to PyPI through Trusted Publishing: no token is stored.
  • Then the MCP registry: server.json carries the same version (a test checks it); run mcp-publisher login github, then mcp-publisher publish. PyPI ownership is proven by the mcp-name: line of the README.

Unofficial project. We are not affiliated, associated, or in any way officially connected with YNAB or any of its subsidiaries or affiliates. The official YNAB website can be found at https://www.ynab.com. The names YNAB and You Need A Budget, as well as related names, tradenames, marks, trademarks, emblems, and images are registered trademarks of YNAB. avenir-mcp is provided as is, without warranty, and is not financial advice. Legal notice · Privacy