Skip to main content
Glama
mossipcams

ynab-mcp-bridge

by mossipcams

ynab-mcp-bridge

ynab-mcp-bridge is a read-only Model Context Protocol (MCP) server for YNAB.

It gives MCP clients a shared YNAB backend over either:

  • stateless HTTP for self-hosted or remote deployments

  • optional OAuth-protected HTTP for remote browser-based MCP clients

  • stdio for local desktop clients and debugging

What You Get

  • Read-only YNAB tools for plans, accounts, categories, payees, transactions, scheduled transactions, and summary views

  • HTTP mode by default

  • Stateless POST /mcp handling that works well with clients that do not keep durable MCP sessions

  • Optional auth2-backed OAuth mode for remote clients such as Claude Web

  • Automatic plan resolution when YNAB_PLAN_ID is not set

Choose A Mode

Mode

Use it when

Auth

Key settings

http + authless

Local use or a trusted self-hosted setup

None

YNAB_API_TOKEN

http + oauth-single-tenant

Remote client access behind an upstream IdP

MCP-side OAuth

MCP_PUBLIC_URL and MCP_AUTH2_CONFIG_PATH

http + oauth-hardened

Same as above, but fail closed unless origins are explicitly allowed

MCP-side OAuth

MCP_PUBLIC_URL, MCP_AUTH2_CONFIG_PATH, and MCP_ALLOWED_ORIGINS

stdio

Local desktop clients and debugging

None

YNAB_API_TOKEN

authless is the default deployment mode. http is the default transport.

Quick Start

1. Install and build

npm install
npm run build
npm run pr:ci

Build artifact policy:

  • dist/ remains tracked in this repository for now because the published package and CLI entrypoints resolve from built JavaScript under dist/.

  • When a source change affects runtime output, keep the generated dist/ artifacts in sync with the source change rather than treating them as disposable local-only files.

  • CI enforces that policy with verify:build-sync after npm run build, and it also runs verify:pack to smoke-test the packed npm artifact before treating a build as release-ready.

  • When CI passes on the exact release commit and your dependency lockfile is already installed locally, pulling the release should be a restart-first path rather than a rebuild-first path.

2. Run the default local HTTP server

export YNAB_API_TOKEN=your-token
npm start

Defaults:

  • host: 127.0.0.1

  • port: 3000

  • path: /mcp

3. Run over stdio

export YNAB_API_TOKEN=your-token
npm run start:stdio

Local CI Preflight

Run this before creating a PR when you want one command that mirrors the required CI gates instead of running individual test commands:

npm run pr:ci

pr:ci runs the required local checks from CI: test:ci, test:coverage, lint:deps, lint, typecheck, lint:unused, and build. It intentionally does not include the advisory-only lint:oxlint step because that CI job is non-blocking.

preflight remains available as a backward-compatible alias, but the default pre-PR workflow should be npm run pr:ci.

For advisory quality reporting outside the blocking preflight gate, you can also run:

npm run lint:duplicates
npm run tech-debt:report

lint:duplicates runs JSCPD across maintained production, tooling, and configuration files using the checked-in .jscpd.json settings. It excludes generated output, Markdown, specs, contracts, and local task docs, and it also ignores the auth2 harness so duplication reporting stays focused on the production module surface. tech-debt:report uses the same advisory boundary and prints the current duplication, dead-export, suppression, debt-marker, and dependency-update counts.

4. Expose authless HTTP intentionally

Use this only when the network path is already trusted.

MCP_TRANSPORT=http \
MCP_HOST=0.0.0.0 \
MCP_ALLOWED_ORIGINS=https://claude.ai \
YNAB_API_TOKEN=your-token \
npm run start:http

5. Enable OAuth for a remote client

This is the recommended remote setup.

MCP_TRANSPORT=http \
MCP_HOST=0.0.0.0 \
MCP_ALLOWED_ORIGINS=https://claude.ai \
MCP_DEPLOYMENT_MODE=oauth-single-tenant \
MCP_PUBLIC_URL=https://mcp.example.com/mcp \
MCP_AUTH2_CONFIG_PATH=./auth2.config.example.json \
YNAB_API_TOKEN=your-token \
npm run start:http

The checked-in auth2.config.example.json shows the canonical auth2 file shape loaded through MCP_AUTH2_CONFIG_PATH. Copy it to a private path, replace the placeholder provider values, and keep /oauth/callback as the callback path.

With an auth2 config file in place, the minimal remote OAuth env surface is:

  • MCP_PUBLIC_URL

  • MCP_AUTH2_CONFIG_PATH

Legacy MCP_OAUTH_* settings are still supported as compatibility overrides, but they are no longer required for the primary auth2 HTTP path.

Environment Variables

YNAB Backend

Variable

Required

Notes

YNAB_API_TOKEN

Yes

Shared backend YNAB token

YNAB_PLAN_ID

No

Default plan for tools that accept planId

If YNAB_PLAN_ID is unset, the bridge tries YNAB's default_plan first, then the only available plan when exactly one exists. If a configured plan becomes stale, the bridge retries once with a fresh resolution.

Transport and HTTP

Variable

Default

Notes

MCP_TRANSPORT

http

http or stdio

MCP_HOST

127.0.0.1

HTTP only

MCP_PORT

3000

HTTP only

MCP_PATH

/mcp

HTTP only

MCP_ALLOWED_ORIGINS

empty

Comma-separated browser origin allowlist

MCP_ALLOWED_HOSTS

empty

Optional comma-separated Host header allowlist

MCP_DEPLOYMENT_MODE

authless

authless, oauth-single-tenant, or oauth-hardened

MCP_AUTH_MODE

none

Legacy compatibility shim: none or oauth

Notes:

  • When an Origin header is present, HTTP mode validates it.

  • Loopback origins are allowed automatically for loopback hosts.

  • oauth-hardened refuses to start without MCP_ALLOWED_ORIGINS.

OAuth

Variable

Required

Notes

MCP_PUBLIC_URL

Yes in OAuth modes

Public MCP URL, for example https://mcp.example.com/mcp

MCP_AUTH2_CONFIG_PATH

Yes in OAuth modes

Path to the auth2 JSON config file

MCP_OAUTH_AUDIENCE

No

Defaults to MCP_PUBLIC_URL

MCP_OAUTH_STORE_PATH

No

Defaults to ~/.ynab-mcp-bridge/oauth-store.json

MCP_OAUTH_TOKEN_SIGNING_SECRET

No

Defaults to a stable derived secret

MCP_OAUTH_CALLBACK_PATH

No

Defaults to /oauth/callback

MCP_OAUTH_SKIP_LOCAL_CONSENT

No

Set to true to bypass the bridge-hosted approval screen for every OAuth client

MCP_OAUTH_SCOPES

No

Comma-separated runtime scope override; defaults to the auth2 config scopes

MCP_OAUTH_CLOUDFLARE_DOMAIN

Optional compatibility override

Shortcut for Cloudflare Access endpoint derivation

MCP_OAUTH_ISSUER

Optional compatibility override

Upstream issuer

MCP_OAUTH_AUTHORIZATION_URL

Optional compatibility override

Upstream authorization endpoint

MCP_OAUTH_TOKEN_URL

Optional compatibility override

Upstream token endpoint

MCP_OAUTH_JWKS_URL

Optional compatibility override

Upstream JWKS endpoint

MCP_OAUTH_CLIENT_ID

Optional compatibility override

Upstream confidential client ID

MCP_OAUTH_CLIENT_SECRET

Optional compatibility override

Upstream confidential client secret

How HTTP Mode Behaves

  • The default HTTP transport is stateless. Clients should send POST /mcp and should not depend on durable MCP sessions.

  • The bridge still exposes Mcp-Session-Id headers for compatibility, but session continuity is not the primary path.

  • In OAuth modes, the bridge acts as the MCP authorization server and exposes:

    • /.well-known/oauth-authorization-server

    • /register

    • /authorize

    • /token

    • /.well-known/oauth-protected-resource/mcp

  • OAuth here protects access to a shared backend YNAB token. It does not do per-user YNAB OAuth delegation.

  • MCP_OAUTH_SKIP_LOCAL_CONSENT=true is intended for owner-operated deployments where you want the bridge to forward every authorization request upstream without showing the local approval page.

Cloudflare Access

For Cloudflare Access, the simplest setup is:

  • MCP_PUBLIC_URL

  • MCP_AUTH2_CONFIG_PATH

Put the Cloudflare issuer, authorization endpoint, token endpoint, JWKS URI, client ID, and client secret in the auth2 config file. The legacy MCP_OAUTH_* settings remain available if you need to override those values at runtime.

Important details:

  • Register the callback built from MCP_PUBLIC_URL and MCP_OAUTH_CALLBACK_PATH, for example https://mcp.example.com/oauth/callback

  • Keep MCP_PUBLIC_URL on the external HTTPS hostname, not the internal bind address

  • Use the public MCP URL as the audience unless your Access app expects a different resource identifier

  • Do not use the older tenant-wide /cdn-cgi/access/sso/oauth2/* endpoints for this flow

If Cloudflare injects Cf-Access-Jwt-Assertion, the bridge can translate that assertion into a bridge-local token as an explicit compatibility path.

Tool Coverage

The server exposes a read-only YNAB toolset across:

  • user and plan metadata

  • plan settings and plan months

  • accounts, categories, and payees

  • transactions and scheduled transactions

  • payee locations

  • money movement and transfer summaries

  • higher-level financial summaries such as spending, cash flow, income, goal progress, obligations, and budget health

For YNAB-style summaries, treat assigned_vs_spent as a timing and buffering signal, not a score for budget discipline. In buffered budgets it often reflects paycheck timing, category staging, or money reserved for future months rather than overspending or underspending by itself.

CLI Examples

Start with the default HTTP settings:

node dist/index.js

Start over stdio:

node dist/index.js --transport stdio

Start over HTTP explicitly:

node dist/index.js --transport http --host 127.0.0.1 --port 3000 --path /mcp

Run the local HTTP reliability probe:

npm run reliability:http -- --requests 10 --concurrency 2

The reliability command uses a bounded authless HTTP scenario that:

  • starts a local bridge unless you pass --url

  • runs initialize, tools/list, and ynab_get_mcp_version

  • prints attempts, failures, error rate, and latency percentiles

  • exits non-zero when the error rate is above --max-error-rate

Useful flags:

  • --requests <n>: number of reliability sequences to run. Each sequence performs three MCP operations.

  • --concurrency <n>: number of sequences to run in parallel.

  • --max-error-rate <0..1>: fail the run if the observed error rate exceeds this threshold.

  • --url <http-url>: target an already running bridge instead of starting a local one.

  • --host, --port, --path: override the local server bind address when --url is not used.

Write a machine-readable smoke artifact and compare against a prior run:

npm run reliability:http -- \
  --requests 10 \
  --concurrency 2 \
  --json-out artifacts/reliability/smoke.json \
  --baseline-artifact artifacts/reliability/baseline-smoke.json

Run the heavier reliability suite in dry-run mode:

npm run reliability:load -- \
  --profile baseline \
  --url http://127.0.0.1:3000/mcp \
  --json-out artifacts/reliability/baseline.json \
  --dry-run

The dedicated load suite is designed for named profiles instead of ad hoc request counts:

  • smoke: fast local regression check using the built-in Node probe

  • baseline: repeatable average-load run for comparisons

  • stress: higher sustained load to expose overload behavior

  • spike: sudden burst behavior

  • soak: longer steady-state run to catch degradation over time

Recommended workflow:

  • run smoke on local changes

  • record baseline artifacts on a stable environment

  • run stress and spike before higher-risk releases

  • run soak on a scheduled cadence or before major rollout events

Thresholds should be evaluated with error rate and latency percentiles such as p95 and p99, not averages alone. The smoke command and artifact comparison flow already follow that model, and the load-suite dry run prints the exact thresholds that would be enforced by the heavier external runner.

Allow specific browser origins:

node dist/index.js \
  --transport http \
  --host 0.0.0.0 \
  --port 3000 \
  --path /mcp \
  --allowed-origins https://claude.ai,https://chat.openai.com

Lock down accepted host headers too:

node dist/index.js \
  --transport http \
  --host 0.0.0.0 \
  --port 3000 \
  --path /mcp \
  --allowed-origins https://claude.ai \
  --allowed-hosts mcp.example.com

Enable OAuth with explicit upstream endpoints:

node dist/index.js \
  --transport http \
  --host 0.0.0.0 \
  --port 3000 \
  --path /mcp \
  --allowed-origins https://claude.ai \
  --deployment-mode oauth-single-tenant \
  --public-url https://mcp.example.com/mcp \
  --oauth-issuer https://example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/client-123 \
  --oauth-authorization-url https://example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/client-123/authorization \
  --oauth-token-url https://example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/client-123/token \
  --oauth-jwks-url https://example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/client-123/jwks \
  --oauth-client-id cloudflare-access-client-id \
  --oauth-client-secret cloudflare-access-client-secret \
  --oauth-audience https://mcp.example.com/mcp \
  --oauth-store-path /var/lib/ynab-mcp-bridge/oauth-store.json \
  --oauth-token-signing-secret replace-with-a-long-random-secret \
  --oauth-scopes openid,profile

For a stricter remote deployment, switch oauth-single-tenant to oauth-hardened.

Docker

Build the image:

docker build -t ynab-mcp-bridge .

Run the default HTTP server:

docker run --rm \
  -p 3000:3000 \
  -e YNAB_API_TOKEN=your-token \
  ynab-mcp-bridge

Rate Limiting

YNAB documents a limit of 200 requests per rolling hour per access token. The bridge applies a shared per-token sliding-window limiter and retries 429 Too Many Requests responses conservatively.

Development

npm test
npm run build
npm run lint:duplicates
npm run tech-debt:report

lint:duplicates runs a JSCPD baseline across maintained production, tooling, and configuration files. It excludes generated/vendor paths, Markdown, specs, contracts, local task docs, and the auth2 harness so the duplicate baseline tracks the production-facing codebase. tech-debt:report prints the current advisory duplicate-remediation baseline together with dead-export and suppression counts so local cleanup work has one repeatable snapshot command.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
0dRelease cycle
93Releases (12mo)

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mossipcams/ynab-mcp-bridge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server