Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `uv run pytest` — Run all tests
- `uv run pytest tests/test_foo.py::test_name` — Run a single test
- `uv run python -m src.server` — Run MCP server standalone (stdio transport)
- `uv run python -m src.cli auth login|status|logout` — Manage the stored token
- `uv run python -m src.cli cache purge` — Delete the cache DB and its key

**Environment:** Requires `YNAB_API_KEY` in `.env.local`
**Environment:** The token comes from the OS credential store (`mcp-ynab auth
login`) or, with a deprecation warning, from `YNAB_API_KEY` in `.env.local`.
Other flags: `YNAB_ALLOW_ENV_KEY=0` (credential store only), `YNAB_READ_ONLY=1`
(write tools not registered), `YNAB_CACHE_ENCRYPTION=0` (plaintext cache,
debugging only).

## Architecture

Expand All @@ -27,10 +33,26 @@ This repo contains two things:
- **`ynab_client.py`** — Async httpx client for YNAB API v1.
- **`cache/`** — 4-tier caching: TTL-based response cache, delta sync (server knowledge tracking), retry with exponential backoff, SQLite persistence.
- **`models/`** — Pydantic models, each module exports a `*_DEFAULT_EXCLUDE` set defining the fields hidden from MCP responses by default. The registry in `src/server/_shared.py` (`DEFAULT_EXCLUDES`) maps model classes to their default exclude sets. Every MCP tool accepts an optional `exclude_fields: list[str]` param that, when provided, fully replaces the default. See `FIELDS.md` for the per-model field reference.
- **`config.py`** — `Settings` via pydantic-settings. Cache DB path is platform-specific (`~/Library/Application Support/ynab-mcp-server/cache.db` on macOS).
- **`security/`** — `credentials.py` (keyring-backed store for the token and the
cache key), `redaction.py` (scrubs live secrets from every error path),
`crypto.py` (AES-256-GCM for the cache), `consent.py` + `diff.py` (human
confirmation with a real before/after diff). See `SECURITY.md` for the threat
model and its stated limits.
- **`cli.py`** — the `mcp-ynab` entry point. A bare invocation starts the stdio
server; `auth`/`cache` subcommands manage credentials and the cache. Server
modules are imported lazily so `auth login` works before a token exists.
- **`config.py`** — `Settings` via pydantic-settings. `ynab_api_key` is a
`SecretStr` resolved from the credential store first, then the environment.
Cache DB path is platform-specific (`~/Library/Application Support/ynab-mcp-server/cache.db` on macOS).

### Key conventions

- **Milliunits:** YNAB stores money as milliunits (1000 = $1.00). The Python server accepts dollars in tool parameters and converts to milliunits internally.
- **Month format:** YNAB months use first-of-month dates (`2026-03-01` for March 2026).
- **Mutating tools require consent:** the 16 write tools take a `ctx: Context | None = None`
parameter (injected by the SDK, invisible in the tool's input schema) and call
`consent.confirm()` before writing. The pre-image must come from
`_shared.client`, never the cache — a stale row would show the user a false
"before". Register them with `_shared.mutating_tool()`, not `_shared.mcp.tool()`,
so `YNAB_READ_ONLY=1` can drop them at registration time.
- **Dependency bounds:** runtime deps in `pyproject.toml` carry upper bounds. An unbounded `mcp[cli]>=1.26.0` broke every fresh install when the SDK shipped 2.0.0 (#21). `uv run pytest` uses `uv.lock` and cannot catch this class of break; `python scripts/smoke_test.py` resolves fresh from the declared constraints and can. Run it before releasing, and after touching any dependency.
78 changes: 70 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,41 @@ An MCP server that connects AI assistants to your [YNAB](https://www.ynab.com/)
- **Search & analytics** — text search across transactions, per-category spending breakdowns, Sankey flow data
- **Bulk operations** — create or update multiple transactions in a single call
- **Dollar amounts** — accepts dollars in parameters, converts to YNAB milliunits internally
- **Token in the OS credential store** — not in a config file any agent can read
- **Human confirmation for every write** — with a real before/after diff, read live from YNAB
- **Encrypted cache** — cached budget data is AES-256-GCM at rest

## Quick Start

Store your [YNAB personal access token](https://app.ynab.com/settings/developer)
once, in your OS credential store:

```bash
uv tool run mcp-ynab auth login
```
uv tool run mcp-ynab

The token is read hidden, validated against the YNAB API, and then saved to
Windows Credential Manager, macOS Keychain, or Secret Service. It does not go
into any config file, and it is not read from `argv` or left in shell history.

Nothing appears as you type or paste — that is intended. If your terminal will
not paste into a hidden prompt (the Windows console sends Ctrl+V as a control
character rather than pasting), feed it from a file instead:

```bash
uv tool run mcp-ynab auth login --stdin < token.txt
```

Requires a [YNAB personal access token](https://app.ynab.com/settings/developer) set as `YNAB_API_KEY`.
```bash
uv tool run mcp-ynab auth status # which source is in use, plus a fingerprint
uv tool run mcp-ynab auth logout # remove the stored token
uv tool run mcp-ynab cache purge # delete the local cache and its encryption key
```

## Configuration

No API key goes in the config — `auth login` already stored it.

### Claude Desktop / ChatGPT

Add to your config file:
Expand All @@ -32,10 +56,7 @@ Add to your config file:
"mcpServers": {
"ynab": {
"command": "uv",
"args": ["tool", "run", "mcp-ynab"],
"env": {
"YNAB_API_KEY": "your-api-key-here"
}
"args": ["tool", "run", "mcp-ynab"]
}
}
}
Expand All @@ -44,11 +65,48 @@ Add to your config file:
### Claude Code

```bash
claude mcp add-json ynab --scope user '{"type":"stdio","command":"uv","args":["tool","run","mcp-ynab"],"env":{"YNAB_API_KEY":"your-api-key-here"}}'
claude mcp add-json ynab --scope user '{"type":"stdio","command":"uv","args":["tool","run","mcp-ynab"]}'
```

See [mcp-ynab.com](https://mcp-ynab.com) for config file locations and troubleshooting.

### Environment variables

| Variable | Default | Effect |
|---|---|---|
| `YNAB_API_KEY` | unset | Still honoured, with a deprecation warning on stderr. Putting it in an MCP client config writes your token in plaintext into a file any agent can read — prefer `auth login`. |
| `YNAB_ALLOW_ENV_KEY` | `1` | Set to `0` to refuse `YNAB_API_KEY` entirely, so the credential store is the only accepted source. |
| `YNAB_READ_ONLY` | `0` | Set to `1` to stop the 16 write tools being registered at all. They never reach the model's tool list. |
| `YNAB_CACHE_ENCRYPTION` | `1` | Set to `0` to store the cache in plaintext. Debugging only. |

## Writes require your confirmation

Every tool that changes something in YNAB asks you first, showing the current
state read live from the YNAB API alongside exactly what would change:

```
CONFIRM: Update transaction
Plan: bud-1

Current state, read from the YNAB API just now:
2026-03-15 Chez Nowhere Groceries -$50.25 cleared=uncleared approved=no

This will change:
amount : -$50.25 -> -$60.00
approved : no -> yes

Confirm to apply exactly this. Nothing else will be sent.
```

Bulk operations list every row rather than summarising a count. Declining, or a
client that cannot ask you, blocks the write — there is no fallback that applies
it unconfirmed.

This relies on your MCP client supporting elicitation. **On a client that does
not, writes will refuse outright.** See [SECURITY.md](./SECURITY.md) for the full
threat model, including what the credential store does and does not protect
against.

## Available Tools

| Group | Tools |
Expand Down Expand Up @@ -80,11 +138,15 @@ uv sync
# Run tests
uv run pytest

# Fresh-resolution smoke test (resolves from pyproject.toml, not uv.lock)
python scripts/smoke_test.py

# Run server standalone
uv run python -m src.server
```

Requires `YNAB_API_KEY` in `.env.local` for running the server.
Running the server needs a token: `uv run python -m src.cli auth login`, or
`YNAB_API_KEY` in `.env.local`.

## License

Expand Down
Loading