YNAB MCP Server
<p align="center">
<img src="assets/icon.png" width="80" height="80" alt="YNAB">
</p>
<h1 align="center">MCP Server for YNAB</h1>
<p align="center">
<strong>Turn natural-language budget questions into safe, local YNAB API workflows</strong><br>
<em>Give your AI assistant read-only budget access by default, with explicit write opt-in</em>
</p>
<p align="center">
<code>67 tools + 4 discovery helpers</code> •
<code>6 guided prompts</code> •
<code>undo journal</code> •
<code>45 read-only operations by default</code> •
<code>read-only by default</code>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@oliverames/mcp-server-for-ynab"><img src="https://img.shields.io/npm/v/%40oliverames%2Fmcp-server-for-ynab?style=flat-square&color=f5a542" alt="npm"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-f5a542?style=flat-square" alt="License"></a>
<a href="https://www.buymeacoffee.com/oliverames"><img src="https://img.shields.io/badge/Buy_Me_a_Coffee-support-f5a542?style=flat-square&logo=buy-me-a-coffee&logoColor=white" alt="Buy Me a Coffee"></a>
</p>
<p align="center">
<a href="#quick-start">Quick Start</a> •
<a href="#install-as-a-plugin">Plugin</a> •
<a href="#2-install-in-claude-code">Claude Code</a> •
<a href="#3-install-in-codex">Codex</a> •
<a href="#other-plugin-hosts">Other Hosts</a> •
<a href="#what-you-can-do">What You Can Do</a> •
<a href="#tools-reference">Tools Reference</a> •
<a href="#environment-variables">Configuration</a>
</p>
---
Run YNAB through Claude Code, Codex, Hermes, Antigravity, or any stdio MCP host. For clients that need a remote URL, the hosted connector at [`https://ames-ynab.link/mcp`](https://ames-ynab.link) signs each user in through YNAB OAuth, with no personal access token to copy into another service. Both paths use the same rate-aware tool layer and convert amounts between dollars and YNAB milliunits. Direct registration starts read-only, and the undo journal records supported changes. Some operations still require manual recovery in YNAB.
## Why This Exists
YNAB's budgeting philosophy works best when you interact with your budget frequently, but the app interface is not designed for quick questions or careful bulk cleanup. "How much did I spend on groceries this month?" should not require navigating three screens. "Categorize all my Amazon orders from this week" should not become a manual, one-by-one review.
This server gives your AI assistant a local interface to YNAB's API, turning natural language into structured budget review and, when explicitly enabled, budget operations. It is designed for real budgeting work: finding overspending, reviewing unapproved transactions, checking category drift, investigating recurring payments, and making verified batch updates without giving the assistant broader access than it needs.
All monetary values are automatically converted between dollars and YNAB's internal milliunits format so the AI never has to think about it. The server uses the [official YNAB JavaScript SDK](https://github.com/ynab/ynab-sdk-js) where it fits, plus direct API calls for newer endpoints and query parameters that the SDK has not caught up with yet.
---
## Quick Start
Requires Node.js 20 or later for local use. Add this to an MCP client that supports stdio servers:
```json
{
"mcpServers": {
"ynab": {
"command": "npx",
"args": ["-y", "@oliverames/mcp-server-for-ynab@latest"],
"env": {
"YNAB_OP_PATH": "op://Development/YNAB/credential"
}
}
}
}
```
This example uses the 1Password CLI. Replace the reference with your own item, or inject `YNAB_API_TOKEN` through your launcher. Ask for `ynab_auth_status` first to inspect credential status without a YNAB API call.
This package stands on its own as a stdio MCP server. You can register the npm package directly and let your MCP client launch it on demand. Plugin packages are also provided for separately managed Claude Code and Codex marketplaces, plus the included Hermes and Antigravity catalogs. You do not need the older `ames-connectors` marketplace for YNAB.
The npm package is the local, owner-run option. It uses a personal access token because the account owner runs the process. The same repository also powers a hosted OAuth connector for clients that accept remote MCP URLs.
### Connect to the hosted remote server
Add this Streamable HTTP URL to Claude.ai, ChatGPT, Mistral Vibe Work, or another remote MCP client:
```text
https://ames-ynab.link/mcp
```
The old `https://ynab.amesvt.com/mcp` endpoint stays available during cutover
validation and will then retire. Add the new URL as a new connection and
authorize there; OAuth tokens stay bound to the hostname where they were issued.
Stored credentials and journals are preserved, but a new consent has its own
isolated journal. See the
[migration and rollback plan](docs/domain-migration.md).
The client opens this connector's consent page and then sends you to YNAB to sign in. Leave the write-access box unchecked for a read-only connection, or enable it when you need the write tools. The connector stores OAuth tokens and undo data with application-layer encryption in Cloudflare KV; the AI client receives only the connector's own scoped token. See the live [privacy policy](https://ames-ynab.link/privacy), [data-deletion flow](https://ames-ynab.link/delete), and [deployment documentation](worker/README.md).
The [July 15 acceptance record](docs/YNAB-COMPLETE-WORKLOG-2026-07-15.md) documents earlier signed-in checks. It isn't a current availability check. The Worker supports scoped authorization, encrypted token storage, and account deletion. High-impact tools retain their `confirmed: true` input requirement after write access is enabled.
### Install as a Plugin
Claude and Codex plugin packages remain available for distribution through a separately managed marketplace. This repository no longer supplies their root catalogs. For direct use, register the npm package with your MCP client.
The plugin starts `@oliverames/mcp-server-for-ynab@latest` and preserves the prior `ames-ynab` connector behavior by setting `YNAB_ALLOW_WRITES=1`. Direct MCP registration remains read-only unless you explicitly enable writes.
### Other Plugin Hosts
The repository also carries host-specific marketplace and plugin manifests for Hermes and Antigravity:
| Host | Marketplace | Plugin manifest | MCP config |
|---|---|---|---|
| Claude Code | Separately managed | `.claude-plugin/plugin.json` | `.mcp.json` |
| Codex | Separately managed | `codex/.codex-plugin/plugin.json` | `codex/.codex-plugin/mcp.json` |
| Hermes | `.hermes-plugin/marketplace.json` | `.hermes-plugin/plugin.json` | `.hermes-plugin/mcp.json` |
| Antigravity | `.antigravity-plugin/marketplace.json` | `.antigravity-plugin/plugin.json` | `.antigravity-plugin/mcp_config.json` |
### 1. Get a YNAB Personal Access Token
Go to [YNAB Developer Settings](https://app.ynab.com/settings/developer) and create a new personal access token.
Do not ask another YNAB user for a personal access token. If you are building a public connector for accounts you do not own, use YNAB OAuth instead.
Credential lookup order:
1. Values passed directly to the MCP process, such as `YNAB_API_TOKEN`.
2. The detected host's plaintext settings: Codex reads `~/.codex/config.toml`, first `[shell_environment_policy.set]`, then `[mcp_servers.ynab.env]`; Claude Code reads `~/.claude/settings.json` under top-level `env`.
3. The other supported agent config as a fallback, useful when a token is already stored locally but the launcher did not inject it.
4. `YNAB_API_TOKEN_FILE`, if configured in any of the sources above.
5. `YNAB_OP_PATH`, if configured in any of the sources above and the `op` CLI is available.
If no token is found, `ynab_auth_status` returns a structured setup guide. Agents should first ask whether the user already has the YNAB token in a password manager such as 1Password. If yes, ask permission before configuring `YNAB_OP_PATH`; otherwise ask the user to add `YNAB_API_TOKEN` to the correct Codex or Claude config file and restart the MCP server.
### 2. Install in Claude Code
Use user scope if you want the server available in all Claude Code projects:
```bash
claude mcp add ynab --scope user \
-e YNAB_API_TOKEN=your-token-here \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
Add a default budget ID if you do not want tools to use YNAB's `last-used` budget:
```bash
claude mcp add ynab --scope user \
-e YNAB_API_TOKEN=your-token-here \
-e YNAB_BUDGET_ID=optional-default-budget-id \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
Use `--scope project` instead of `--scope user` if you want Claude Code to write a project-local `.mcp.json`.
If `~/.claude/settings.json` already contains `env.YNAB_API_TOKEN`, you may omit `-e YNAB_API_TOKEN=...`; the server will read the Claude setting as a fallback if the launcher does not inject it.
Verify Claude Code can see the server:
```bash
claude mcp get ynab
```
If Claude Code reports that `ynab` already exists, remove the old entry and run the add command again:
```bash
claude mcp remove ynab
```
### 3. Install in Codex
Register the same npm package directly with Codex:
```bash
codex mcp add ynab \
--env YNAB_API_TOKEN=your-token-here \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
With a default budget ID:
```bash
codex mcp add ynab \
--env YNAB_API_TOKEN=your-token-here \
--env YNAB_BUDGET_ID=optional-default-budget-id \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
If `~/.codex/config.toml` already contains `YNAB_API_TOKEN` under `[shell_environment_policy.set]` or `[mcp_servers.ynab.env]`, you may omit `--env YNAB_API_TOKEN=...`; the server will read the Codex setting as a fallback if the launcher does not inject it.
Verify Codex can see the server:
```bash
codex mcp get ynab
```
If Codex reports that `ynab` already exists, remove the old entry and run the add command again:
```bash
codex mcp remove ynab
```
### 4. Enable Write Tools (Optional)
By default, the server registers read-only tools only. To expose tools that create, update, import, or delete YNAB data, add `YNAB_ALLOW_WRITES=1` when you register the server:
```bash
claude mcp add ynab --scope user \
-e YNAB_API_TOKEN=your-token-here \
-e YNAB_ALLOW_WRITES=1 \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
```bash
codex mcp add ynab \
--env YNAB_API_TOKEN=your-token-here \
--env YNAB_ALLOW_WRITES=1 \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
Every write requires an exact preview from `preview_write_tool`, its session-bound single-use `previewToken`, and `confirmed: true` after explicit human approval of the plan. A token is not human consent. Execution rereads fresh state and rejects changed IDs, budget or values; `expectedMatchedCount` is an additional check, never a replacement. Pending bank imports cannot be previewed exactly; initiate them in YNAB and review the imported rows here.
### Manual JSON Config
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"ynab": {
"command": "npx",
"args": ["-y", "@oliverames/mcp-server-for-ynab"],
"env": {
"YNAB_API_TOKEN": "your-token-here"
}
}
}
}
```
**Generic MCP client**:
```json
{
"mcpServers": {
"ynab": {
"command": "npx",
"args": ["-y", "@oliverames/mcp-server-for-ynab"],
"env": {
"YNAB_API_TOKEN": "your-token-here"
}
}
}
}
```
If you prefer a global install, point your MCP client at the package binary directly:
```bash
npm install -g @oliverames/mcp-server-for-ynab
```
```json
{
"mcpServers": {
"ynab": {
"command": "mcp-server-for-ynab",
"env": {
"YNAB_API_TOKEN": "your-token-here",
"YNAB_BUDGET_ID": "optional-default-budget-id"
}
}
}
}
```
### Docker (Optional)
The repository ships a `Dockerfile` (also used by registry-hosted builds such as Glama). The container speaks MCP over stdio:
```bash
docker build -t mcp-server-for-ynab .
docker run -i --rm -e YNAB_API_TOKEN=your-token-here mcp-server-for-ynab
```
Add `-e YNAB_ALLOW_WRITES=1` to enable write tools, and `-e YNAB_BUDGET_ID=...` for a default budget.
### 1Password Token Lookup (Optional)
If your token is stored in 1Password, set `YNAB_OP_PATH` instead of `YNAB_API_TOKEN`. The `op` CLI must be installed and authenticated in the environment that launches the MCP server.
```bash
claude mcp add ynab --scope user \
-e YNAB_OP_PATH="op://Personal/YNAB API Token/credential" \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
```bash
codex mcp add ynab \
--env 'YNAB_OP_PATH=op://Personal/YNAB API Token/credential' \
-- npx -y @oliverames/mcp-server-for-ynab@latest
```
### Local Smoke Test
From this repo, you can verify the published npm package without changing any MCP client config:
```bash
YNAB_API_TOKEN=your-token-here npm run smoke:review-unapproved -- --published
```
---
## What You Can Do
| Ask your AI... | What happens under the hood |
|---|---|
| "How much did I spend on groceries this month?" | `search_categories` → `get_month_category` |
| "Show me all unapproved transactions" | `review_unapproved` groups by readiness |
| "Log a $50 Costco trip under groceries" | `search_payees` → `search_categories` → `create_transaction` |
| "Record my $250 Visa payment from checking" | `create_transaction` with `transferToAccountName: "Visa"` (the server resolves the transfer payee) |
| "Find the $12.34 charge from last week" | `search_transactions` with `amount: 12.34` (or `query: "onion river"`) |
| "Set up monthly $1,500 rent on the 1st" | `create_scheduled_transaction` with `monthly` frequency |
| "Move $200 from emergency fund to dining" | `search_categories` → `update_month_category` (x2) |
| "Categorize all my Amazon orders from this week" | `get_transactions` (filtered) → `update_transactions` (batch) |
| "Create a 'Side Projects' spending category" | `search_categories` (find group) → `create_category` |
| "How has my budget been re-allocated this month?" | `get_money_movements_by_month` |
| "What subscriptions am I actually paying for?" | `detect_recurring_charges` finds payee + amount + cadence patterns with annual cost |
| "How am I doing financially?" | `get_budget_health` scores savings rate, age of money, and overspending green/yellow/red |
| "Are my credit card payments fully funded?" | `audit_credit_card_payments` compares card balances to payment categories |
| "Merge my duplicate coffee categories" | `merge_category` moves transactions and budgets in one confirmed call |
| "Split that Costco charge across three categories" | `prepare_split_for_matching` mirrors the imported transaction for UI matching |
| "Undo that last batch approval" | `list_undo_history` → `undo_operation` restores the journaled before-state |
| "Import my latest bank transactions" | `import_transactions` triggers linked account sync |
---
## Features
The source defines 67 tools for the YNAB v1 REST API and four discovery helpers, for 71 registered MCP operations. Read-only mode exposes 42 YNAB operations and three helpers. Enabling writes adds 25 YNAB operations and the write-execution helper. The server also defines MCP prompts and resources:
| Resource | Tools | Capabilities |
|----------|-------|-------------|
| **Budgets** | 4 | List, view details, settings |
| **Accounts** | 3 | List, view, create |
| **Categories** | 9 | Create and update categories/groups, search, goals, monthly budgets |
| **Payees** | 5 | List, view, create, rename, search |
| **Payee Locations** | 3 | GPS coordinates for mobile transactions |
| **Months** | 2 | Monthly summaries with per-category breakdown |
| **Money Movements** | 4 | Budget re-allocation tracking |
| **Transactions** | 12 | Create, read, update, delete, bulk operations, matching, and filters |
| **Scheduled Transactions** | 5 | Full CRUD for recurring transactions |
| **Convenience** | 2 | Unapproved transaction review and overspending checks |
| **Workflows** | 3 | Category merge/retire, split-via-match for imported transactions |
| **Audits** | 2 | Credit card payment funding, reconciliation diagnosis |
| **Analytics** | 3 | Budget health, income/expense + savings rate, recurring-charge detection |
| **Undo & Export** | 3 | Local undo history, supported reversal, CSV export |
| **Tool Discovery** | 4 | Authentication status, tool index, read and write executors |
Beyond tools, the server ships **6 MCP prompts** (guided workflows: monthly review, weekly triage, categorize-and-approve, subscription audit, reconciliation, credit card audit) and **4 MCP resources** (`ynab://guide/*`: YNAB methodology, write-safety rules, audit patterns, review flags reference).
### Design Decisions
- **Read-only by default** - write tools are not registered unless `YNAB_ALLOW_WRITES=1` is set. Read tools are annotated with `readOnlyHint: true`; write tools are annotated with `readOnlyHint: false`, idempotency hints, and destructive hints for delete operations.
- **Server instructions and structured contracts** - the server sends a compact `instructions` block at initialize (dollar amounts, `search_transactions` before unfiltered pulls, `transactionId`, the transfer convenience, write-safety rules), so clients that never open a prompt or resource still get the rules that matter. Every tool exposes a human-readable title, an input schema even when it takes no arguments, a minimal output schema, and matching `structuredContent`. Impact hints describe YNAB as a private, bounded system so app clients can distinguish reads, writes, and destructive operations accurately.
- **Exact write previews and confirmation** - every write requires a session-bound single-use preview token and `confirmed: true` after explicit user approval. Bulk-filter writes also support `expectedMatchedCount` as an additional guard.
- **Dollar amounts everywhere** - inputs and outputs are in dollars (`-12.34`), never milliunits (`-12340`). Conversion is automatic and transparent.
- **Smart budget resolution** - set `YNAB_BUDGET_ID` for a default, or omit it to auto-resolve to your last-used budget. Every tool accepts an optional `budgetId` override.
- **Pinned YNAB host** - all HTTP requests are restricted to `https://api.ynab.com`, redirects are not followed, and API tokens are redacted from surfaced errors.
- **Agent-aware token fallback** - use direct process env, Codex `~/.codex/config.toml`, Claude `~/.claude/settings.json`, a small token file via `YNAB_API_TOKEN_FILE`, or a 1Password CLI reference via `YNAB_OP_PATH`.
- **Split transactions** - first-class support for subtransactions in create, read, and format operations. Updates can also convert a non-split transaction into a split (the YNAB API does not support editing the subtransactions of an existing split).
- **Current transaction filters** - transaction list tools support `sinceDate`, `untilDate`, type filters, resource filters, and delta requests. YNAB defaults omitted `sinceDate` to one year ago, so pass an explicit older date when you need older history.
- **Bulk operations** - `create_transactions` and `update_transactions` handle arrays in a single API call. Bulk updates can look transactions up by `id` or by `importId`.
- **Verified batch updates** - `update_transactions` refetches the whole batch in a single list request after the bulk API call, reports mismatched fields without retrying financial writes, and returns a `verification` block so approval counts cannot hide failed category writes. Journal entries reuse the verified after-state.
- **Fetch-then-merge updates** - scheduled transaction updates (which use PUT semantics) automatically fetch the current state and merge your changes, so you only specify what changed.
- **Transfer convenience** - `create_transaction` and `create_transactions` accept `transferToAccountId` or `transferToAccountName` and resolve the destination account's `transfer_payee_id` themselves, so a credit card payment is one call with no payee lookup. YNAB's own error for a `Transfer -- …` payee name now carries the same pointer.
- **Fuzzy search** - `search_categories` and `search_payees` do case-insensitive partial matching across all entries, and `search_transactions` does the same over transactions (payee, raw import string, memo, account, category, split rows) with an optional absolute-amount match and `limit` / `offset` paging. Category search covers both the category name and its group name, tokenizes multi-word queries and OR-matches them (so `gym fitness membership` still lands), ranks whole-phrase and name hits above single-token and group-only hits, and reports `matched_on` / `matched_terms` per result. It does no synonym expansion, an empty result says so and points at `list_categories`.
- **Decoded text** - YNAB stores some strings HTML-escaped (bank imports are the usual source). Names, memos and notes are entity-decoded on the way out, so a payee reads as `B&H Photo Video` rather than `B&H Photo Video`, and name search matches either spelling. Writes send exactly what the caller supplied.
- **Approval workflow with anomaly flags** - `review_unapproved` scans the full transaction history for unapproved entries (YNAB's API defaults to the last year, which would silently hide older stragglers) and groups transactions into "ready to approve" (categorized, split, or transfer) and "needs attention" (uncategorized), and attaches a `flags` array to each transaction surfacing anomalies: `manually_entered` (not bank-imported), `match_broken` (stale match reference), `scheduled_transaction_realized`, `new_payee`, `no_prior_amount_match` (novel amount for this payee), and `category_drift:was_X` (payee categorized differently in the prior 60 days). Group-level flags aggregate the union of all transaction flags. Bulk approval requires `confirmed: true`.
- **Honest group headers** - a `by_payee` group describes all of its rows, not just the first one. Groups carry `category_names` (every distinct category in the group) plus `mixed_categories`, and `category_name` is `null` whenever the group spans more than one category. `total` is the net; a group whose rows run both directions also carries `mixed_amount_signs: true` with `inflow_total` and `outflow_total`, so a net that nets refunds against charges cannot be read as a single small charge.
- **Nullable updates** - update tools accept `null` for clearable fields (`memo`, `payeeName`, `categoryId`, `flagColor`) to distinguish "don't change" (omit) from "clear this field" (`null`).
- **Target behavior support** - category create/update tools expose `goalNeedsWholeAmount` for YNAB's "Set aside another" vs. "Refill up to" goal behavior.
- **Delta request support** - high-volume list tools accept `lastKnowledgeOfServer` and return `server_knowledge` when that parameter is provided. `get_budget` supports full delta exports: pass `lastKnowledgeOfServer` to receive every entity changed since that knowledge in one response.
- **Undo journal** - every transaction write is journaled locally with before-state (`~/.ynab-mcp-undo.json`); `list_undo_history` reviews it and `undo_operation` reverses a journaled write. Category/payee/scheduled writes are journaled for audit without automatic undo.
- **Prompts and resources** - guided workflow prompts (monthly review, weekly triage, categorize-and-approve, subscription audit, reconciliation, credit card audit) and a general YNAB-methodology knowledge base as MCP resources, so any host gets the working discipline without a separate skill.
- **Rate-budget surfacing** - responses warn the model when 50 or fewer requests remain in the trailing hour, on top of the client-side limiter that enforces the budget.
- **Debt account support** - loan and debt accounts include `debt_original_balance`, `debt_interest_rates`, `debt_minimum_payments`, and `debt_escrow_amounts` with correct unit conversion (rates stay as percentages, payments convert from milliunits).
---
## Tools Reference
Read tools are available by default. Tools that create, update, import, or delete YNAB data are marked as write tools and are registered only when `YNAB_ALLOW_WRITES=1`.
### User & Budgets
| Tool | Description |
|------|-------------|
| `get_user` | Get the authenticated user |
| `list_budgets` | List all budgets with IDs, names, date ranges, format settings, and default budget. Pass `includeAccounts: true` to include each budget's accounts. |
| `get_budget` | Get budget summary (name, currency, account/category/payee counts). Pass `lastKnowledgeOfServer` for a delta export of every changed entity plus the next `server_knowledge`. |
| `get_budget_settings` | Get currency and date format settings |
### Accounts
| Tool | Description |
|------|-------------|
| `list_accounts` | List all accounts with balances, debt details, and import status |
| `get_account` | Get full account details including notes and debt fields |
| `create_account` | Write tool: create a new account (checking, savings, creditCard, mortgage, etc.) |
**Supported account types:** `checking`, `savings`, `cash`, `creditCard`, `lineOfCredit`, `otherAsset`, `otherLiability`, `mortgage`, `autoLoan`, `studentLoan`, `personalLoan`, `medicalDebt`, `otherDebt`
### Categories & Category Groups
| Tool | Description |
|------|-------------|
| `list_categories` | List all category groups and their categories with budgeted/activity/balance. Groups and categories carry `hidden`, `deleted`, and `internal` flags (`internal` marks YNAB's own categories such as Credit Card Payments). |
| `get_category` | Get full category details including goal progress and cadence |
| `get_month_category` | Get category budget for a specific month |
| `update_month_category` | Write tool: set the budgeted amount for a category in a month |
| `update_category` | Write tool: update name, note, goal target, goal target date, or move to a different group. `goalFrequency` (`monthly` / `weekly` / `yearly`, YNAB API 1.86) sets a recurring target; it requires `goalTarget` and cannot be combined with `goalTargetDate`. |
| `create_category` | Write tool: create a new category in an existing group, with an optional goal including a recurring `goalFrequency` target |
| `create_category_group` | Write tool: create a new category group |
| `update_category_group` | Write tool: rename a category group |
| `search_categories` | Case-insensitive partial name search over category names **and** category-group names (e.g., "groc" finds "Groceries"; "health" finds everything in a "Health & Medical" group). Multi-word queries are tokenized and OR-matched, results are ranked, and each result reports `matched_on` / `matched_terms`. Pass `includeHidden: true` to search hidden categories and groups. |
### Payees
| Tool | Description |
|------|-------------|
| `list_payees` | List all payees with transfer account mappings |
| `get_payee` | Get payee details |
| `create_payee` | Write tool: create a new payee |
| `update_payee` | Write tool: rename a payee |
| `search_payees` | Case-insensitive partial name search |
### Payee Locations
| Tool | Description |
|------|-------------|
| `list_payee_locations` | List all payee locations (GPS coordinates from mobile app) |
| `get_payee_location` | Get a specific payee location |
| `get_payee_locations_by_payee` | Get all locations for a specific payee |
### Months
| Tool | Description |
|------|-------------|
| `list_months` | List budget months with income, budgeted, activity, to-be-budgeted, age of money, and notes |
| `get_month` | Get month detail with per-category budget/activity/balance/goal breakdown |
### Money Movements
| Tool | Description |
|------|-------------|
| `list_money_movements` | List all money movements (budget re-allocations between categories) |
| `get_money_movements_by_month` | Get money movements for a specific month |
| `list_money_movement_groups` | List all money movement groups (batched re-allocations) |
| `get_money_movement_groups_by_month` | Get money movement groups for a specific month |
### Transactions
| Tool | Description |
|------|-------------|
| `get_transactions` | Get transactions with filters: by account, category, payee, month, status (`unapproved`/`uncategorized`), `sinceDate`, and `untilDate`. Non-delta results stop at 500 rows by default (`limit` up to 2000, page with `offset`); a capped or paged result returns `{ transactions, total, returned, offset, has_more, next_offset }` instead of a bare array. Prefer `search_transactions` when you are looking for specific rows. |
| `search_transactions` | Server-side search with pagination: `query` is a case-insensitive substring match over payee name, raw bank import string, memo, account, category, and split rows; `amount` matches on absolute value (12.34 finds a -12.34 outflow). Optional `accountId` / `sinceDate` / `untilDate` bound the scan; `limit` (default 50) and `offset` page through `total_matches`, newest first, with `next_offset` for the following page. |
| `get_transaction` | Get a single transaction by ID. The argument is `transactionId`, not `id` (includes subtransactions). Auto-handles composite scheduled-transaction IDs like `uuid_YYYY-MM-DD`; if the underlying matched transaction has been deleted, falls back to returning the active scheduled template wrapped as `{ resource_type: "scheduled_transaction", ... }`. |
| `create_transaction` | Write tool: create a transaction with optional split (subtransactions must sum to total). For a transfer (including a credit card payment) pass `transferToAccountId` or `transferToAccountName` instead of a payee; the server resolves the destination account's internal transfer payee, since YNAB rejects a literal `Transfer -- …` payee name. |
| `create_transactions` | Write tool: bulk create multiple transactions in a single API call (supports split transactions and the same `transferToAccountId` / `transferToAccountName` convenience per entry) |
| `update_transaction` | Write tool: partial update - only specified fields change. Can convert a non-split transaction into a split via `subtransactions`. Composite scheduled-transaction IDs (`uuid_YYYY-MM-DD`) are writable, see below. |
| `update_transactions` | Write tool: batch update multiple transactions at once (look up each entry by `id` or `importId`), then verify requested fields persisted using a single batch refetch. Returns `newly_approved_count` beside `approved_count`. Pass `returnSummary: true` for compact counts instead of full objects on large batches (avoids overflowing the tool-result size limit). |
| `approve_transactions` | Write tool: approve unapproved transactions in bulk by filter (`payeeId` / `categoryId` / `accountId`) without hand-listing IDs. Skips uncategorized transactions by default, requires `confirmed: true`, and supports `expectedMatchedCount`. |
| `reassign_payee_transactions` | Write tool: move all transactions from one payee to another, the merge workaround since the YNAB API has no payee delete/merge endpoint. Requires `confirmed: true` and supports `expectedMatchedCount`. |
| `delete_transaction` | Write tool: delete a transaction. Requires `confirmed: true`. |
| `import_transactions` | Write tool: trigger import from linked bank accounts |
### Scheduled Transactions
| Tool | Description |
|------|-------------|
| `list_scheduled_transactions` | List all recurring transactions |
| `get_scheduled_transaction` | Get a specific scheduled transaction |
| `create_scheduled_transaction` | Write tool: create a recurring transaction with frequency |
| `update_scheduled_transaction` | Write tool: update (fetch-then-merge preserves unchanged fields) |
| `delete_scheduled_transaction` | Write tool: delete a scheduled transaction. Requires `confirmed: true`. |
**Supported frequencies:** `never`, `daily`, `weekly`, `everyOtherWeek`, `twiceAMonth`, `every4Weeks`, `monthly`, `everyOtherMonth`, `every3Months`, `every4Months`, `twiceAYear`, `yearly`, `everyOtherYear`
### Convenience
| Tool | Description |
|------|-------------|
| `preview_write_tool` | Read fresh before-state and exact proposed write intent; returns a five-minute single-use session-bound token. Separate user approval is required. |
| `suggest_transaction_categories` | Read-only payee-history/recency/schedule suggestions with confidence/evidence and uncertainty/split abstention; never auto-applies. |
| `review_unapproved` | Get unapproved transactions grouped by readiness: "ready to approve" (categorized, split, or transfer) vs. "needs category first" (uncategorized). Each transaction includes a `flags` array highlighting anomalies (manually_entered, match_broken, no_prior_amount_match, category_drift, new_payee, scheduled_transaction_realized) computed against 60 days of payee history. Group headers report `category_names` / `mixed_categories` and, when the rows run both directions, `inflow_total` / `outflow_total` beside the net `total`. Includes a warning against blind approval. Pass `summary: true` for counts + by-payee aggregates only, or `compact: true` to keep per-transaction rows (with IDs) while dropping bulky fields so the response fits inline, `match_broken` rows keep `matched_transaction_id` in compact mode, since triaging that flag means looking the matched id up. Above `maxTransactions` unapproved rows (default 400) a full response degrades to `compact`, and above twice that to `summary`; the response reports `mode` and a `notice`. |
| `get_overspent_categories` | Get categories with negative balances for a month, useful for finding prior-month overspending that reduces the current month's Ready to Assign. |
### Workflows (v4.0)
The YNAB API has no category merge/delete endpoint and cannot split an already-imported transaction; these composite tools do everything the API allows and report the remaining manual UI step.
| Tool | Description |
|------|-------------|
| `move_category_budget` **(write)** | Durable exact-value category allocation move for one month, with fresh readback and explicit recovery. |
| `list_operations` | Tenant-isolated durable partial/unknown/completed workflow state; filter by budget or inspect all authorized journal budgets. |
| `resume_operation` **(write)** | Recover verified pending steps after a new preview and explicit approval; conflicts stop safely. |
| `merge_category` **(write)** | Recategorize every transaction from one category into another and move budgeted amounts (`moveBudgetedMonths: none/current/all`, capped at 24 months). The emptied source category is then hidden/deleted by hand in the YNAB UI. Requires `confirmed: true`. |
| `retire_category` **(write)** | Prepare a category for deletion: move its transaction history to a replacement category and zero its budgets (dollars return to Ready to Assign). Requires `confirmed: true`. |
| `prepare_split_for_matching` **(write)** | Create a mirror unapproved split transaction that YNAB will offer to match with an imported original, the only way to get splits onto a bank-imported transaction. Requires `confirmed: true`. |
### Audits & Analytics (v4.0, read-only)
| Tool | Description |
|------|-------------|
| `audit_credit_card_payments` | Compare each credit card's balance against its Credit Card Payment category and report underfunded cards. |
| `audit_account_reconciliation` | Per-account reconciliation status; with `accountId`, lists the exact uncleared/unapproved items to check against the bank statement. |
| `get_budget_health` | Snapshot with green/yellow/red indicators: savings rate, age of money, Ready to Assign, overspending, credit card debt. |
| `get_income_expense_summary` | Net income/spending and savings, with refunds/reversals, localized category identities and split/transfer accounting. Ambiguous metadata requires explicit income category IDs. |
| `get_spending_trends` | Net category/payee monthly trends and carefully labeled unusual-spending observations with threshold evidence. |
| `forecast_scheduled_balances` | Future scheduled-payment account ledger scenarios, including transfer effects and explicit omissions. |
| `detect_recurring_charges` | Find subscriptions/recurring charges from history by payee + amount + cadence, with estimated annual cost. |
| `export_transactions` | Export filtered transactions as CSV text. Capped at `maxRows` (default 500, max 2000); when exceeded the newest rows are kept and a second text block reports the truncation. |
### Undo Journal (v4.0)
Transaction writes retain relevant before/verified-after state in `~/.ynab-mcp-undo.json` when storage is available. Up to 100 recent completed/audit entries are retained; unfinished durable category operations are preserved. The file contains sensitive plaintext financial fields, uses mode-600 permissions and atomic locked replacement, and survives restarts. Hosted journals are encrypted per OAuth consent. Undo history is tenant-isolated; legacy unverified entries cannot be restored automatically.
| Tool | Description |
|------|-------------|
| `list_undo_history` | List journaled writes, newest first, with undo capability per entry. Reads only the local journal. |
| `undo_operation` **(write)** | Reverse a journaled write: restore updated fields, delete created transactions, or recreate a deleted one (without its original bank-import linkage). One undo per entry. Requires `confirmed: true`. |
---
### Resolve broken match references
Pass `matched_transaction_id` values from `review_unapproved` (including compact rows) to `resolve_matched_transactions` as `matchedTransactionIds`, with an optional `budgetId`. Each batch accepts 1–50 IDs and fetches each distinct ID once. It checks budget access first. Results contain the original matched ID and either `orphan` for a transaction 404 or `found` with the formatted transaction in dollars. Found rows require duplicate review before any separate write. Authentication, rate-limit, network, and other failures return an error, not orphan results. The tool performs only reads and never changes match links.
## Workflow Safety Notes
### Write Tool Opt-In
The server starts in read-only mode. Write tools are not merely discouraged; they are absent from `listTools` unless `YNAB_ALLOW_WRITES=1` is present when the MCP process starts. This mirrors the safer hosted-connector pattern: the default permission set can inspect budgets, transactions, categories, payees, months, and scheduled transactions, but it cannot mutate financial data.
If a client already has the process running, changing the environment is not enough. Restart the MCP server after setting or clearing `YNAB_ALLOW_WRITES`.
Call the read-only `preview_write_tool` with a concrete write tool and its input:
```json
{
"tool_name": "update_transactions",
"input": {
"budgetId": "budget-id",
"transactions": [{ "id": "transaction-id", "categoryId": "category-id" }]
}
}
```
Present the returned before-values and proposed changes to the user. After
explicit approval, call that tool with the same input plus `previewToken` from
`preview_token` and `confirmed: true`. Tokens expire after five minutes and
are consumed on an attempted execution, including stale-state rejection.
They cannot move between sessions, budgets, transaction IDs or requested values.
Bank memos and merchant strings are untrusted data and cannot authorize edits.
The server reads fresh state again and restricts outbound mutations to the
approved exact intent. A changed match count or changed values at the same
count require a new preview and approval. YNAB has no conditional writes:
an external change can still race the final read and write.
`move_category_budget`, `merge_category` and `retire_category` persist their
exact steps before writing and read back every step. Use `list_operations` and
a newly approved `resume_operation` after a partial or unknown result; never
blindly repeat a financial operation. Unverified or conflicting undo is refused,
and an uncertain non-idempotent recreation undo is blocked from replay until
manual reconciliation. See [recovery limits](docs/write-recovery.md).
### Read freshness and projections
Transaction, account, category, payee and schedule lists accept `projection: "lean" | "full"`, `freshness: "fresh" | "cached"` and `includeFreshness`. Fresh is the default. Explicit caching uses tenant/session/budget-isolated merged entities and a 30-second TTL. Transaction metadata reports the exact `history_since` boundary; an older requested date expands the baseline, and `complete` applies to the reported coverage. Caller deltas never seed the merged baseline. Writes invalidate the cache even after response loss. See [read-cache details](docs/read-cache.md).
### Batch Updates
When a batch operation categorizes and approves transactions at the same time, do not use `review_unapproved` counts as the only success check. Approved transactions leave the review queue even if a category write failed, so queue counts can hide approved-but-still-uncategorized transactions.
`update_transactions` protects this path by refetching the batch after the bulk API call (one list request for the whole batch, not one request per transaction) and comparing the persisted fields with the requested fields. If anything differs, it reports the conflict or unapplied fields without replaying financial writes. Split verification checks every active line, including requested amount/category/payee/memo; generated line IDs and response order do not affect equality. The response includes:
```json
{
"verification": {
"checked": 1,
"retried": [],
"failed": []
}
}
```
Treat any `failed` entry as a real write failure and inspect the named transaction with `get_transaction`.
#### Approval counts
Batch responses report two different approval numbers, and only one of them answers "what did this call approve?":
```json
{
"updated_count": 9,
"approved_count": 3,
"newly_approved_count": 1,
"already_approved_count": 2,
"approval_state_unknown_count": 0
}
```
`approved_count` is how many rows in the batch are approved **now**, including rows that were already approved when the batch was submitted. `newly_approved_count` is how many rows this call actually flipped, measured against the pre-write fetch; `already_approved_count` and `approval_state_unknown_count` (no before-state available, e.g. an `importId` row whose refetch failed) account for the rest. Report `newly_approved_count` to a user. `approve_transactions` only ever touches rows that were unapproved when it fetched them, so there the two are equal.
### Composite Scheduled-Transaction IDs
A scheduled transaction that has realized carries a composite ID of the form `d9e7c3c2-…_2026-07-30` (the shape `review_unapproved` flags as `scheduled_transaction_realized`). The suffix makes the ID look synthetic, and the natural assumption is that such a row is read-only or that the suffix must be stripped before writing. Neither is true: pass the ID exactly as returned and `update_transaction` / `update_transactions` apply memo, category and approval to the realized transaction, returning the full updated object. Reads are equally forgiving, `get_transaction` strips the suffix itself and falls back to the scheduled template when the underlying matched transaction has been deleted.
### Credit Card Payment Transfers
If two unapproved transactions are clearly a credit card payment plus the matching checking-account outflow, convert them into a transfer before approval. Approving both sides as ordinary categorized transactions preserves the wrong structure and creates cleanup work.
Manual YNAB transfer fixes can replace one side of the pair with a new transaction ID. Read-only verification should not assume both original IDs survive. If one old ID returns `resource_not_found`, inspect recent activity in both involved accounts and verify the pair by `transfer_transaction_id` cross-links.
---
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `YNAB_API_TOKEN` | Yes* | (none) | [Personal access token](https://app.ynab.com/settings/developer) from YNAB Developer Settings. Read from process env first, then supported Codex and Claude plaintext agent settings. |
| `YNAB_API_TOKEN_FILE` | No | (none) | Path to a file containing only the token. The file must be 4 KB or smaller. Used only when `YNAB_API_TOKEN` is unset, and can be provided through process env or agent settings. |
| `YNAB_BUDGET_ID` | No | `last-used` | Default budget ID. If omitted, tools use YNAB's most recently accessed budget. Run `list_budgets` to find IDs. |
| `YNAB_ALLOW_WRITES` | No | read-only | Set to `1` to register write tools. Any other value keeps the server read-only. |
| `YNAB_OP_PATH` | No | (none) | 1Password secret reference for your API token. Used only if no direct token is configured. Can be provided through process env or agent settings. |
| `YNAB_DISABLE_AGENT_CONFIG_FALLBACK` | No | `0` | Set to `1` to stop the server from reading `~/.codex/config.toml` and `~/.claude/settings.json`. Intended for tests and tightly controlled runtimes. |
| `YNAB_RATE_LIMIT_PER_HOUR` | No | `190` | Client-side rate limiter. Set to `0` to disable for controlled tests. |
| `YNAB_RATE_LIMIT_BURST` | No | `10` | Maximum burst size before rate limiting pauses requests. |
| `YNAB_HTTP_TIMEOUT_MS` | No | `30000` | Per-request timeout in milliseconds. Set to `0` to disable the timeout. |
| `YNAB_HTTP_RETRIES` | No | `2` | Automatic retries for retryable failures. HTTP 429 (rate limited) retries any request because YNAB rejected it before processing; 502/503/504 and network errors retry reads (`GET`/`HEAD`) only. Honors `Retry-After`. Set to `0` to disable. |
| `YNAB_MAX_RESPONSE_BYTES` | No | `8388608` | Maximum direct-fetch response size for newer endpoints. |
*`YNAB_API_TOKEN` is required unless `YNAB_API_TOKEN_FILE` or `YNAB_OP_PATH` is set. These values may come from direct process env, Codex config, or Claude settings.
### Configuration Validation
Numeric environment variables are parsed at startup. A value that is not a number,
or that falls below the minimum for its setting, is ignored: the server keeps the
documented default and writes a warning to stderr naming the variable, the offending
value, and the fallback it used. This covers `YNAB_RATE_LIMIT_PER_HOUR`,
`YNAB_RATE_LIMIT_BURST`, `YNAB_HTTP_TIMEOUT_MS`, `YNAB_HTTP_RETRIES`, and
`YNAB_MAX_RESPONSE_BYTES`.
Zero is a valid, documented setting for `YNAB_RATE_LIMIT_PER_HOUR`,
`YNAB_HTTP_TIMEOUT_MS`, and `YNAB_HTTP_RETRIES`, so it is accepted rather than
replaced.
### Input Validation
Write tools reject over-long text before the request reaches YNAB, using the limits
in YNAB's own API specification:
- **Payee names on transactions** (`payeeName`): 200 characters
- **Payee names on `create_payee` and `update_payee`**: 500 characters
- **Category group names**: 50 characters
- **Memos**: 500 characters
The two payee limits differ because YNAB's API sets them differently. The
transaction endpoints cap `payee_name` at 200, while the payee endpoints cap the
payee resource name at 500.
### 1Password Integration
If you store your YNAB token in [1Password CLI](https://developer.1password.com/docs/cli/), set `YNAB_OP_PATH` to your secret reference and omit `YNAB_API_TOKEN`:
```json
{
"mcpServers": {
"ynab": {
"command": "npx",
"args": ["-y", "@oliverames/mcp-server-for-ynab"],
"env": {
"YNAB_OP_PATH": "op://Personal/YNAB API Token/credential"
}
}
}
}
```
The server invokes `op read` once per process when it resolves `YNAB_OP_PATH`. MCP hosts can start a separate process for each session, so concurrent sessions can each prompt for 1Password authorization. The server does not share a token cache between processes.
The fallback adds ~1-2s to startup. If `op` is unavailable or the item is not found, `ynab_auth_status` reports the lookup problem and returns setup guidance instead of letting a normal YNAB tool fail with a generic unauthorized error. If no token source is configured, the setup guide tells the calling agent to ask whether you have a token in 1Password or another password manager, request permission before editing agent config, and otherwise ask you to add `YNAB_API_TOKEN` to the appropriate Codex or Claude settings file.
---
## Amount Handling
All amounts in tool inputs and outputs are in **dollars** (e.g., `-12.34` for a $12.34 outflow). The server converts to/from YNAB's internal milliunits format automatically.
| Direction | Sign | Example |
|-----------|------|---------|
| Outflow (spending) | Negative | `-50.00` |
| Inflow (income) | Positive | `2500.00` |
| Transfer out | Negative | `-1000.00` |
| Transfer in | Positive | `1000.00` |
---
## Rate Limiting
The YNAB API allows **200 requests per hour** per access token, enforced on a rolling window. This server applies a client-side limiter at 190 requests per hour with a burst of 10 by default. Read tools typically use one request. Write previews, fresh-state checks and verification add reads. Batch update verification uses one bounded list request, with individual lookups for older rows outside that window. Creation verification and durable workflows can read each affected entity or step. Large workflows may require later explicit recovery after interruption or rate limiting.
If a request still hits YNAB's limit (HTTP 429), the server waits for the `Retry-After` interval and retries automatically (up to `YNAB_HTTP_RETRIES` times). Transient 502/503/504 responses and network failures are retried for read requests only, since a failed write may have partially applied on the server.
Set `YNAB_RATE_LIMIT_PER_HOUR=0` only for controlled local tests or smoke checks where you know you will stay under YNAB's API limit.
When the trailing-hour budget drops to 50 requests or fewer, tool responses append a pacing warning so the calling model can switch to delta requests, summary modes, and batch tools before hitting the wall.
---
## Architecture
```
┌─────────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ AI Assistant │────▶│ MCP Server for │────▶│ YNAB API │
│ │ │ YNAB │ │ │
│ (Claude, GPT, etc) │◀────│ (this package) │◀────│ api.ynab.com│
└─────────────────────┘ └──────────────────┘ └──────────────┘
MCP stdio transport HTTPS/REST
```
- **Transport:** stdio (standard MCP server pattern)
- **Auth:** Bearer token via process env, Codex or Claude agent config, `YNAB_API_TOKEN_FILE`, or `YNAB_OP_PATH` for local owner-run use
- **SDK:** Official [`ynab`](https://www.npmjs.com/package/ynab) v4.5.0 or compatible for core endpoints, with direct `fetch` for additional endpoints and transaction filters
- **Safety:** read-only default, explicit write opt-in, confirmation gates for destructive and bulk-filter writes, host-pinned HTTPS requests to `api.ynab.com`, no redirect following, redacted token errors
- **Validation:** All parameters validated with [Zod](https://zod.dev) schemas
- **Error handling:** API errors are caught, formatted, and returned as MCP error responses with detail messages
The hosted OAuth connector runs on Cloudflare Workers at [`ames-ynab.link`](https://ames-ynab.link). Its implementation notes are in [worker/README.md](worker/README.md) and [docs/hosted-oauth-connector.md](docs/hosted-oauth-connector.md). For data handling details for the local package, see [docs/privacy.md](docs/privacy.md).
The Cloudflare connector is separate from a private container deployment. Its availability also depends on the YNAB application's authorization limits and approval state. Repository code doesn't establish the current upstream approval status.
### Glama Hosting
The repo is ready for [Glama](https://glama.ai) MCP hosting: the root [`glama.json`](glama.json) claims the registry listing (per [Glama's glama.json spec](https://glama.ai/blog/2025-07-08-what-is-glamajson)), and the [`Dockerfile`](Dockerfile) is what Glama's GitHub integration builds. To deploy: Glama dashboard → MCP Hosting → deploy from GitHub → select this repo, then set environment variables `YNAB_API_TOKEN` (required), `YNAB_BUDGET_ID` (recommended), and `YNAB_DISABLE_AGENT_CONFIG_FALLBACK=1` (no agent config files exist in the container). Leave `YNAB_ALLOW_WRITES` unset until you have verified the deployment read-only, and keep the deployment **private** because its env vars hold your personal token. `YNAB_OP_PATH` is unsupported in hosted containers (no 1Password CLI); the server reports this explicitly and falls back to discovery-only mode rather than crashing. Glama wraps the stdio transport as a Streamable HTTP Gateway endpoint automatically.
### Distribution Scope
The local package, hosted OAuth connector, and host-specific plugin manifests have different credential and write-access settings. Review the selected route before distributing it. Public directory approval is separate from publishing source or an npm package.
---
## Testing
### Offline Tests (no YNAB account required)
Unit tests cover the pure helpers (amount conversion, ID normalization, update verification, config parsing, URL safety, executor input validation), and the safety-model tests boot the real server over stdio to verify the read-only default, write-tool gating, annotations, and credential fallback behavior:
```bash
npm run test:unit
npm run test:safety
cd worker && npm test
```
The root suites run in CI (`.github/workflows/ci.yml`) on Node 20, 22, and 24 for pushes to main and every pull request, along with `release:check` and a credential-free MCP smoke test. The Worker suite covers consent-page escaping, OAuth state/PKCE, encrypted consent-scoped Durable Object credentials and CAS journals, multi-client/multi-user boundaries, refresh rotation, failed authorization rollback, and ownership-verified deletion. Legacy shared grants require deliberate reconnection after deployment.
`npm run test:eval` runs scripted synthetic agent trajectories through real MCP and loopback fake HTTP. It makes no model-provider calls and does not claim to benchmark model reasoning. The stateful fake suite covers imports/splits, historical delta cache, applied-write timeouts, partial failures, stale previews, undo conflicts and malicious memo instructions. See [harness details](docs/testing-harness.md) and the [approved coverage checklist](docs/improvement-coverage.md).
### Live Integration Tests
The integration test suite runs against a live YNAB budget. Most write tests create temporary transactions and delete or restore them, but category and category group creation is not reversible through the public API and is skipped unless explicitly enabled.
```bash
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm test
```
Use `YNAB_TEST_BUDGET_ID` to target a dedicated test budget without changing your server default. To include category and category group creation coverage, run with `YNAB_RUN_NONREVERSIBLE_TESTS=1`.
Every live write in `npm test` and `smoke:batch-verify`, including cleanup or restoration, displays a fresh exact preview and requires you to type `yes` in an interactive terminal. Environment flags only expose tools; they do not approve writes. Without a terminal, write calls fail before execution while read calls remain available. If cleanup is declined or a preview becomes stale, inspect the reported test transaction in YNAB. Pending bank imports cannot be previewed exactly and must be performed in YNAB.
Tests cover all tool categories: reads, reversible writes, bulk operations, search, split transactions, scheduled transaction CRUD with fetch-then-merge verification, money movements, and payee locations.
### MCP Smoke Tests
Use the smoke tests when you need to prove the server is reachable over stdio without reconstructing a custom MCP client. These commands use the official MCP SDK client, the same transport shape used by normal MCP hosts. `smoke:list-tools` can run without a live token to verify discovery, but live read and write smokes need a token from process env, supported Codex or Claude settings, `YNAB_API_TOKEN_FILE`, or `YNAB_OP_PATH`.
```bash
YNAB_DISABLE_AGENT_CONFIG_FALLBACK=1 npm run smoke:list-tools
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:review-unapproved
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id YNAB_ALLOW_WRITES=1 npm run smoke:batch-verify
```
To test the package currently published to npm instead of the local checkout:
```bash
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:list-tools -- --published
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:review-unapproved -- --published
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id YNAB_ALLOW_WRITES=1 npm run smoke:batch-verify -- --published
```
`smoke:list-tools` verifies that high-value read tools such as `review_unapproved`, `get_transactions`, `search_transactions`, `search_categories`, and `search_payees` are present. When `YNAB_ALLOW_WRITES=1` is set, it also verifies `update_transactions`. `smoke:review-unapproved` calls `review_unapproved` with `summary: true` and prints only aggregate counts. `smoke:batch-verify` creates a temporary transaction, uses `update_transactions` to categorize and approve it in one call, refetches it through the MCP server, and deletes it afterward.
---
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for the local checks and pull request guidelines. Report suspected vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
```bash
git clone https://github.com/oliverames/ynab-mcp-server.git
cd ynab-mcp-server
npm install
YNAB_API_TOKEN=your-token npm start
```
### Dependencies
- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk) - MCP server framework
- [`ynab`](https://www.npmjs.com/package/ynab) - Official YNAB JavaScript client
- [`zod`](https://www.npmjs.com/package/zod) - Tool input and output schemas
The server uses JavaScript ES modules and has no compilation step.
### Release Checks
Before publishing, run:
```bash
npm run sync:plugin
npm run release:check
npm pack --dry-run
```
After publishing, run `npm run release:check:registry` to verify the npm `latest` dist-tag and repo metadata agree on the same version. `npm run build:mcpb` remains available for an explicit local bundle, but the normal install path is direct MCP registration through npm.
Linear records the verified GitHub release and MCPB bundle in this repository's Ames Consulting (AME) release pipeline. npm publication and the hosted OAuth Worker require separate receiving checks. See [release reporting](.github/RELEASES.md) for issue references, credentials and reporting-only recovery.
Pushing a `v*` tag triggers the release workflow (`.github/workflows/release.yml`), which verifies the tag against `package.json`, re-runs the offline tests and consistency checks, builds the MCPB bundle, and publishes a GitHub release with the bundle attached.
---
## Privacy and Non-Affiliation
See [docs/privacy.md](docs/privacy.md) for this connector's data handling, deletion, and token-use details.
This connector is 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](https://www.ynab.com/).
The names YNAB and You Need A Budget, as well as related names, trade names, marks, trademarks, emblems, and images are registered trademarks of YNAB.
---
## License
[MIT](LICENSE)
---
<p align="center">
<a href="https://www.buymeacoffee.com/oliverames">
<img src="https://img.shields.io/badge/Buy_Me_a_Coffee-support-f5a542?style=for-the-badge&logo=buy-me-a-coffee&logoColor=white" alt="Buy Me a Coffee">
</a>
</p>
<p align="center">
<sub>
Built by <a href="https://ames.consulting">Oliver Ames</a> in Vermont
• <a href="https://github.com/oliverames">GitHub</a>
• <a href="https://linkedin.com/in/oliverames">LinkedIn</a>
• <a href="https://bsky.app/profile/oliverames.bsky.social">Bluesky</a>
</sub>
</p>
TDQS
Scored across 39 tools
Most tools have clearly distinct resource+action pairs (list/get/search variants), and descriptions explicitly guide selection (e.g. 'prefer search_transactions to find specific rows'). The main mild risk is the cluster of aggregate reporting tools (income/spending summary, budget health, recurring detection, audits) and get_transactions/search_transactions/export_transactions, but each has a distinct output or purpose.
The dominant pattern is verb_noun (list_budgets, get_category, search_payees, export_transactions, audit_credit_card_payments, detect_recurring_charges). Minor deviations include get_transactions instead of list_transactions, get_payee_locations_by_payee, and the ynab_tool_* meta tools, but these are readable and mostly predictable.
With 39 tools, the set feels heavy, especially since several are near-duplicate scoped variants (four money-movement tools, four payee-location tools, plus search and export variants). While the YNAB domain is broad, this count exceeds a comfortable size and many tools could be consolidated without losing clarity.
The server is heavily read-only: it covers lookup, search, audit, and reporting well, but there are no actual transaction/category/account write tools despite mentions of write capability and references to actions like update_month_category and approval. Core lifecycle operations for editing transactions, approving entries, or updating budgets are missing, which leaves significant workflow gaps.