Set up ynab-mcp
About five minutes: get a token from YNAB, add a few lines to your Claude configuration, restart.
Requirements
- A YNAB account.
- Node.js 22.14.0 or later for the npx and source options. Check with
node -v. Or use Docker instead, which needs no Node.js. - An MCP client: Claude Desktop, Claude Code, or another client that supports MCP servers.
The package is ynab-mcp. The npm package ynab-mcp-server is an unrelated project.
Get a YNAB personal access token
- Sign in to YNAB on the web and open Account Settings, Developer Settings.
- Under Personal Access Tokens, choose New Token and confirm your password.
- Copy the token. YNAB shows it only once. Treat it like a password: anyone holding it can read and change your budget.
You can revoke the token on the same page at any time. A revoked token stops working immediately.
Claude Desktop
Open the configuration file from the Claude menu (Settings, Developer, Edit Config), or edit it directly:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add one of the following, replace your_token_here, save, and quit and reopen Claude Desktop. If the file already has an mcpServers section, add the "ynab" entry inside it.
Using npx (recommended)
{
"mcpServers": {
"ynab": {
"command": "npx",
"args": ["-y", "ynab-mcp"],
"env": {
"YNAB_ACCESS_TOKEN": "your_token_here"
}
}
}
}
Using Docker
{
"mcpServers": {
"ynab": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "YNAB_ACCESS_TOKEN",
"ghcr.io/auzroz/ynab-mcp:latest"
],
"env": {
"YNAB_ACCESS_TOKEN": "your_token_here"
}
}
}
}
The -e YNAB_ACCESS_TOKEN argument passes the value from env into the container without putting it on the command line. Image tags follow releases: latest, or a version such as 0.4.2.
Using a local build
{
"mcpServers": {
"ynab": {
"command": "node",
"args": ["/path/to/ynab-mcp/dist/index.js"],
"env": {
"YNAB_ACCESS_TOKEN": "your_token_here"
}
}
}
}
See From source for building it.
Claude Code
These commands are for Bash or Zsh; on Windows, run them in WSL or Git Bash. Type this first command rather than copying it, so your token stays on the clipboard, then paste the token at its prompt, which doesn't echo it or keep it in shell history:
read -rs YNAB_ACCESS_TOKEN
Then register the server:
claude mcp add ynab -e YNAB_ACCESS_TOKEN="$YNAB_ACCESS_TOKEN" -- npx -y ynab-mcp
unset YNAB_ACCESS_TOKEN
This registers the server for you in the current project. Add --scope user to use it everywhere. Claude Code saves the token in its own configuration (~/.claude.json), as Claude Desktop does in its config file. Run claude mcp list to check it connected, or /mcp inside a session.
To add write access later, remove it with claude mcp remove ynab and add it again with -e YNAB_READ_ONLY=false as a second -e option.
Other MCP clients
Any client that launches stdio MCP servers can run ynab-mcp. Give it this command and set YNAB_ACCESS_TOKEN in the server's environment:
npx -y ynab-mcp
Or with Docker (keep -i: stdio needs an open standard input):
docker run -i --rm -e YNAB_ACCESS_TOKEN ghcr.io/auzroz/ynab-mcp:latest
From source
# Clone the repository
git clone https://github.com/auzroz/ynab-mcp.git
cd ynab-mcp
# Install dependencies and build
npm install
npm run build
The server (stdio) needs your token in the environment. Type this command rather than copying it, so your token stays on the clipboard, then paste the token at its prompt, which doesn't echo it or keep it in history (Bash or Zsh; on Windows, WSL or Git Bash):
read -rs YNAB_ACCESS_TOKEN
Then start it:
export YNAB_ACCESS_TOKEN
npm start
To try tools interactively without Claude, use MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.js
Contributor setup, tests and standards are in CONTRIBUTING.md.
HTTP mode
By default the server speaks stdio to a client on the same machine. Set MCP_TRANSPORT=http to run it as a remote server over MCP's Streamable HTTP transport at POST /mcp, with GET /health for load balancers.
For a quick personal setup, header mode takes your YNAB token per request:
MCP_TRANSPORT=http PORT=3000 YNAB_ACCESS_TOKEN=… npm start
# connect an MCP client via the mcp-remote shim:
npx mcp-remote http://localhost:3000/mcp --header "X-YNAB-Token: <your-token>"
Serve it behind TLS. For claude.ai, for several people, or for anything reachable from the internet, use OAuth mode: see Self-hosting.
Read-only and write mode
ynab-mcp starts read-only. Claude can look at everything but change nothing: write tools are left out of the tool list, and the server refuses them even if a client calls one by name.
To let Claude create, update and delete, add YNAB_READ_ONLY to the server's environment and restart the client:
"env": {
"YNAB_ACCESS_TOKEN": "your_token_here",
"YNAB_READ_ONLY": "false"
}
- Accepted values are
true,false,1,0,yesandno. Anything else, such as a typo, stops the server at startup rather than guessing. - Update and delete tools are marked destructive, because YNAB has no undo. Most clients ask before running them.
- Every write is recorded in the audit log. Ask Claude to
show the audit log
to see what changed.
A good habit: keep your everyday setup read-only, and turn writes on when you want Claude to do bookkeeping.
Configuration reference
All settings are environment variables. Most people only need the first one.
All modes
| Variable | Default | What it does |
|---|---|---|
YNAB_ACCESS_TOKEN | none | Your YNAB personal access token. Required for stdio. In HTTP header mode it is the fallback when a request has no X-YNAB-Token. |
YNAB_BUDGET_ID | last-used | Budget to use when Claude doesn't name one. A budget UUID, last-used or default. |
YNAB_READ_ONLY | true | Set to false to allow writes. See Read-only and write mode. |
CACHE_TTL_MS | 300000 | How long accounts, categories, payees and the budget list are cached, in milliseconds (5 minutes). |
RATE_LIMIT_PER_HOUR | 180 | Requests per hour the server allows itself. YNAB's limit is 200; the default keeps a 10% margin. |
LOG_LEVEL | info | debug, info, warn or error (stdio mode). |
MCP_TRANSPORT | stdio | stdio for a local client, or http for a remote server. |
HTTP mode
| Variable | Default | What it does |
|---|---|---|
PORT | 3000 | Port to listen on. |
PUBLIC_URL | none | The public HTTPS address of your server, such as https://ynab-mcp.example.com. Required for OAuth mode, where it is also the OAuth issuer. |
MCP_RESOURCE_URL | PUBLIC_URL + /mcp | The MCP endpoint's absolute URL, which tokens are bound to. |
ALLOWED_HOSTS | the PUBLIC_URL host; localhost too when PUBLIC_URL is unset or loopback | Comma-separated Host header allowlist. An explicit list replaces the defaults. |
ALLOWED_ORIGINS | the PUBLIC_URL origin; without PUBLIC_URL, https:// for each allowed host, plus http:// for localhost | Comma-separated Origin allowlist for /mcp. Requests with no Origin header aren't checked. |
ENABLE_DNS_REBINDING_PROTECTION | true | Host and Origin checks. Leave on; behind a proxy, forward Host or set ALLOWED_HOSTS. |
TRUST_PROXY | 0 | Number of reverse-proxy hops to trust for X-Forwarded-*, such as 1 behind a single Caddy or nginx. Set it only when the server is reachable solely through the proxy (publish no ports), or clients can spoof X-Forwarded-For. |
MCP_MAX_SESSIONS | 100 | Maximum concurrent MCP sessions. |
MCP_MAX_SESSIONS_PER_USER | 10 | Maximum sessions per user; past it the user's least recently used session closes. Must not exceed MCP_MAX_SESSIONS. |
MCP_SESSION_IDLE_TIMEOUT_MS | 1800000 | Idle time before a session closes (30 minutes). |
OAuth mode (multi-user)
OAuth mode turns on when YNAB_OAUTH_CLIENT_ID, YNAB_OAUTH_CLIENT_SECRET, ENCRYPTION_KEY and PUBLIC_URL are all set.
| Variable | Default | What it does |
|---|---|---|
YNAB_OAUTH_CLIENT_ID | none | Client ID of your YNAB OAuth application. |
YNAB_OAUTH_CLIENT_SECRET | none | Its client secret. |
ENCRYPTION_KEY | none | 32-byte key that encrypts stored YNAB tokens. Generate one with openssl rand -base64 32 and keep it stable. |
YNAB_OAUTH_ALLOW_WRITE | true | Offer read-write at the consent screen. false makes every connection read-only. |
STORAGE_DRIVER | memory | memory (lost on restart, for trying it out), sqlite or postgres. |
SQLITE_PATH | none | Database file for sqlite, such as /data/ynab-mcp.db. |
DATABASE_URL | none | Connection string for postgres. |
MCP_ACCESS_TOKEN_TTL_SEC | 3600 | Lifetime of the access tokens the server issues to MCP clients. |
MCP_AUTH_CODE_TTL_SEC | 600 | Lifetime of an authorization code and of the consent step. |
Numbers must be plain digits (10ms is rejected), and booleans must be one of the accepted words. The server stops with an error that names the variable but never echoes its value.
Troubleshooting
Claude doesn't show the YNAB tools
- Quit Claude Desktop completely (not just the window) and reopen it.
- Check the config file is valid JSON: a missing comma or a trailing comma stops every server from loading.
- Read the server log. On macOS it is in
~/Library/Logs/Claude/(mcp-server-ynab.log); on Windows in%APPDATA%\Claude\logs\. - Check
node -vprints 22.14 or later. Claude Desktop may use a different Node.js than your terminal; if in doubt, use the Docker option.
"YNAB_ACCESS_TOKEN is required"
The token didn't reach the server. Make sure it is inside the env block of the "ynab" entry and spelled exactly YNAB_ACCESS_TOKEN. With Docker, both the -e YNAB_ACCESS_TOKEN argument and the env entry are needed.
Every request fails with an authentication error
The token was revoked, mistyped or copied with extra spaces. Create a new one in YNAB's Developer Settings and paste it again.
Claude says it can't add or change anything
That's the read-only default working. See Read-only and write mode.
"Invalid boolean value for YNAB_READ_ONLY"
The value isn't one of true, false, 1, 0, yes or no. The server refuses to start rather than guess whether you meant to allow writes.
Claude is looking at the wrong budget
Without a budget named, the server uses the one you opened most recently in YNAB. Ask Claude to list my budgets
and name the one you want, or set YNAB_BUDGET_ID to its ID.
"Rate limit" errors
YNAB allows 200 requests an hour per token, and the server stops at 180. Long analysis sessions can reach it; ask Claude to check the rate limit status
to see when it resets. Caching and delta sync keep repeat questions cheap.
HTTP mode answers every request with 403
The Host header isn't on the allowlist, usually because a reverse proxy rewrote it. Forward the original Host, or add the name to ALLOWED_HOSTS (include the public host too, as an explicit list replaces the defaults).
Still stuck?
Ask Claude to run a health check
, which tests the connection to YNAB. Then open an issue with the version, how you run it, and what happened. Remove your token, budget names, payees and amounts from anything you paste.