@oliverames/ynab-mcp-server 1.7.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,18 +6,18 @@
6
6
 
7
7
  <p align="center">
8
8
  <strong>The complete Model Context Protocol server for YNAB</strong><br>
9
- <em>Give your AI assistant full access to your budget</em>
9
+ <em>Give your AI assistant read-only budget access by default, with explicit write opt-in</em>
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <code>44 tools</code> &bull;
13
+ <code>47 tools with writes enabled</code> &bull;
14
14
  <code>100% API coverage</code> &bull;
15
15
  <code>YNAB API v1.83</code>
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
19
  <a href="https://www.npmjs.com/package/@oliverames/ynab-mcp-server"><img src="https://img.shields.io/npm/v/%40oliverames%2Fynab-mcp-server?style=flat-square&color=f5a542" alt="npm"></a>
20
- <a href="https://github.com/oliverames/ynab-mcp-server/releases/tag/v1.4.0"><img src="https://img.shields.io/github/v/release/oliverames/ynab-mcp-server?style=flat-square&color=f5a542&label=MCPB" alt="MCPB release"></a>
20
+ <a href="https://github.com/oliverames/ynab-mcp-server/releases/tag/v2.1.1"><img src="https://img.shields.io/github/v/release/oliverames/ynab-mcp-server?style=flat-square&color=f5a542&label=MCPB" alt="MCPB release"></a>
21
21
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-f5a542?style=flat-square" alt="License"></a>
22
22
  <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>
23
23
  </p>
@@ -26,7 +26,7 @@
26
26
  <a href="#quick-start">Quick Start</a> &bull;
27
27
  <a href="#install-with-mcpb">MCPB Download</a> &bull;
28
28
  <a href="#what-you-can-do">What You Can Do</a> &bull;
29
- <a href="#tools-reference">All 44 Tools</a> &bull;
29
+ <a href="#tools-reference">Tools Reference</a> &bull;
30
30
  <a href="#environment-variables">Configuration</a>
31
31
  </p>
32
32
 
@@ -36,7 +36,7 @@
36
36
 
37
37
  YNAB's budgeting philosophy works best when you interact with your budget frequently - but the app interface isn't designed for quick queries or bulk operations. "How much did I spend on groceries this month?" shouldn't require navigating three screens. "Categorize all my Amazon orders from this week" shouldn't be a manual, one-by-one process.
38
38
 
39
- This server gives your AI assistant full access to YNAB's API, turning natural language into budget operations. All monetary values are automatically converted between dollars and YNAB's internal milliunits format so the AI never has to think about it. Built on the [official YNAB JavaScript SDK](https://github.com/ynab/ynab-sdk-js) with direct API calls for the newest endpoints (category creation, category groups, money movements) that the SDK hasn't caught up with yet.
39
+ This server gives your AI assistant a safe interface to YNAB's API, turning natural language into budget review and, when explicitly enabled, budget operations. All monetary values are automatically converted between dollars and YNAB's internal milliunits format so the AI never has to think about it. Built on the [official YNAB JavaScript SDK](https://github.com/ynab/ynab-sdk-js) with direct API calls for the newest endpoints (category creation, category groups, money movements) that the SDK hasn't caught up with yet.
40
40
 
41
41
  ---
42
42
 
@@ -44,11 +44,11 @@ This server gives your AI assistant full access to YNAB's API, turning natural l
44
44
 
45
45
  ### Install with MCPB
46
46
 
47
- For Claude Desktop and other MCPB-compatible clients, download the local bundle from the [v1.4.0 release](https://github.com/oliverames/ynab-mcp-server/releases/tag/v1.4.0):
47
+ For Claude Desktop and other MCPB-compatible clients, download the local bundle from the [v2.1.1 release](https://github.com/oliverames/ynab-mcp-server/releases/tag/v2.1.1):
48
48
 
49
- [Download `ynab-mcp-server-1.4.0.mcpb`](https://github.com/oliverames/ynab-mcp-server/releases/download/v1.4.0/ynab-mcp-server-1.4.0.mcpb)
49
+ [Download `ynab-mcp-server-2.1.1.mcpb`](https://github.com/oliverames/ynab-mcp-server/releases/download/v2.1.1/ynab-mcp-server-2.1.1.mcpb)
50
50
 
51
- The bundle includes the YNAB favicon, production runtime dependencies, and setup prompts for your personal access token and optional default budget ID.
51
+ The bundle includes the YNAB favicon, production runtime dependencies, and setup prompts for your personal access token, optional default budget ID, and optional write-tool opt-in.
52
52
 
53
53
  ### 1. Get a YNAB Personal Access Token
54
54
 
@@ -110,6 +110,23 @@ npm install -g @oliverames/ynab-mcp-server
110
110
 
111
111
  That's it. Your AI can now talk to YNAB.
112
112
 
113
+ 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` to the MCP server environment:
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "ynab": {
119
+ "command": "npx",
120
+ "args": ["-y", "@oliverames/ynab-mcp-server"],
121
+ "env": {
122
+ "YNAB_API_TOKEN": "your-token-here",
123
+ "YNAB_ALLOW_WRITES": "1"
124
+ }
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
113
130
  ---
114
131
 
115
132
  ## What You Can Do
@@ -131,7 +148,7 @@ That's it. Your AI can now talk to YNAB.
131
148
 
132
149
  ## Features
133
150
 
134
- **Complete YNAB API v1.83 coverage** with 44 tools:
151
+ **Complete YNAB API v1.83 coverage** with 47 tools when writes are enabled:
135
152
 
136
153
  | Resource | Tools | Capabilities |
137
154
  |----------|-------|-------------|
@@ -144,14 +161,18 @@ That's it. Your AI can now talk to YNAB.
144
161
  | **Money Movements** | 4 | Budget re-allocation tracking |
145
162
  | **Transactions** | 8 | Full CRUD, bulk ops, split transactions, multi-filter |
146
163
  | **Scheduled Transactions** | 5 | Full CRUD for recurring transactions |
147
- | **Convenience** | 1 | Unapproved transaction review workflow |
164
+ | **Convenience** | 2 | Unapproved transaction review and overspending checks |
148
165
 
149
166
  ### Design Decisions
150
167
 
168
+ - **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.
151
169
  - **Dollar amounts everywhere** - inputs and outputs are in dollars (`-12.34`), never milliunits (`-12340`). Conversion is automatic and transparent.
152
170
  - **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.
171
+ - **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.
172
+ - **Token fallback options** - use `YNAB_API_TOKEN`, a small token file via `YNAB_API_TOKEN_FILE`, or a 1Password CLI reference via `YNAB_OP_PATH`.
153
173
  - **Split transactions** - first-class support for subtransactions in create, read, and format operations.
154
174
  - **Bulk operations** - `create_transactions` and `update_transactions` handle arrays in a single API call.
175
+ - **Verified batch updates** - `update_transactions` refetches every requested transaction after the bulk API call, retries mismatched fields once through `update_transaction`, and returns a `verification` block so approval counts cannot hide failed category writes.
155
176
  - **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.
156
177
  - **Fuzzy search** - `search_categories` and `search_payees` do case-insensitive partial matching across all entries.
157
178
  - **Approval workflow with anomaly flags** - `review_unapproved` 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.
@@ -164,6 +185,8 @@ That's it. Your AI can now talk to YNAB.
164
185
 
165
186
  ## Tools Reference
166
187
 
188
+ 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`.
189
+
167
190
  ### User & Budgets
168
191
 
169
192
  | Tool | Description |
@@ -179,7 +202,7 @@ That's it. Your AI can now talk to YNAB.
179
202
  |------|-------------|
180
203
  | `list_accounts` | List all accounts with balances, debt details, and import status |
181
204
  | `get_account` | Get full account details including notes and debt fields |
182
- | `create_account` | Create a new account (checking, savings, creditCard, mortgage, etc.) |
205
+ | `create_account` | Write tool: create a new account (checking, savings, creditCard, mortgage, etc.) |
183
206
 
184
207
  **Supported account types:** `checking`, `savings`, `cash`, `creditCard`, `lineOfCredit`, `otherAsset`, `otherLiability`, `mortgage`, `autoLoan`, `studentLoan`, `personalLoan`, `medicalDebt`, `otherDebt`
185
208
 
@@ -190,11 +213,11 @@ That's it. Your AI can now talk to YNAB.
190
213
  | `list_categories` | List all category groups and their categories with budgeted/activity/balance |
191
214
  | `get_category` | Get full category details including goal progress and cadence |
192
215
  | `get_month_category` | Get category budget for a specific month |
193
- | `update_month_category` | Set the budgeted amount for a category in a month |
194
- | `update_category` | Update name, note, goal target, goal target date, or move to a different group |
195
- | `create_category` | Create a new category in an existing group (with optional goal) |
196
- | `create_category_group` | Create a new category group |
197
- | `update_category_group` | Rename a category group |
216
+ | `update_month_category` | Write tool: set the budgeted amount for a category in a month |
217
+ | `update_category` | Write tool: update name, note, goal target, goal target date, or move to a different group |
218
+ | `create_category` | Write tool: create a new category in an existing group (with optional goal) |
219
+ | `create_category_group` | Write tool: create a new category group |
220
+ | `update_category_group` | Write tool: rename a category group |
198
221
  | `search_categories` | Case-insensitive partial name search (e.g., "groc" finds "Groceries") |
199
222
 
200
223
  ### Payees
@@ -203,8 +226,8 @@ That's it. Your AI can now talk to YNAB.
203
226
  |------|-------------|
204
227
  | `list_payees` | List all payees with transfer account mappings |
205
228
  | `get_payee` | Get payee details |
206
- | `create_payee` | Create a new payee |
207
- | `update_payee` | Rename a payee |
229
+ | `create_payee` | Write tool: create a new payee |
230
+ | `update_payee` | Write tool: rename a payee |
208
231
  | `search_payees` | Case-insensitive partial name search |
209
232
 
210
233
  ### Payee Locations
@@ -236,13 +259,15 @@ That's it. Your AI can now talk to YNAB.
236
259
  | Tool | Description |
237
260
  |------|-------------|
238
261
  | `get_transactions` | Get transactions with filters: by account, category, payee, month, or status (`unapproved`/`uncategorized`) |
239
- | `get_transaction` | Get a single transaction by ID (includes subtransactions). Auto-handles composite scheduled-transaction IDs like `uuid_YYYY-MM-DD`. |
240
- | `create_transaction` | Create a transaction with optional split (subtransactions must sum to total) |
241
- | `create_transactions` | Bulk create multiple transactions in a single API call (supports split transactions) |
242
- | `update_transaction` | Partial update - only specified fields change |
243
- | `update_transactions` | Batch update multiple transactions at once |
244
- | `delete_transaction` | Delete a transaction |
245
- | `import_transactions` | Trigger import from linked bank accounts |
262
+ | `get_transaction` | Get a single transaction by 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", ... }`. |
263
+ | `create_transaction` | Write tool: create a transaction with optional split (subtransactions must sum to total) |
264
+ | `create_transactions` | Write tool: bulk create multiple transactions in a single API call (supports split transactions) |
265
+ | `update_transaction` | Write tool: partial update - only specified fields change |
266
+ | `update_transactions` | Write tool: batch update multiple transactions at once, then refetch and verify requested fields persisted. Pass `returnSummary: true` for compact counts instead of full objects on large batches (avoids overflowing the tool-result size limit). |
267
+ | `approve_transactions` | Write tool: approve unapproved transactions in bulk by filter (`payeeId` / `categoryId` / `accountId`) without hand-listing IDs. Skips uncategorized transactions by default; returns a compact summary. |
268
+ | `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. |
269
+ | `delete_transaction` | Write tool: delete a transaction |
270
+ | `import_transactions` | Write tool: trigger import from linked bank accounts |
246
271
 
247
272
  ### Scheduled Transactions
248
273
 
@@ -250,9 +275,9 @@ That's it. Your AI can now talk to YNAB.
250
275
  |------|-------------|
251
276
  | `list_scheduled_transactions` | List all recurring transactions |
252
277
  | `get_scheduled_transaction` | Get a specific scheduled transaction |
253
- | `create_scheduled_transaction` | Create a recurring transaction with frequency |
254
- | `update_scheduled_transaction` | Update (fetch-then-merge preserves unchanged fields) |
255
- | `delete_scheduled_transaction` | Delete a scheduled transaction |
278
+ | `create_scheduled_transaction` | Write tool: create a recurring transaction with frequency |
279
+ | `update_scheduled_transaction` | Write tool: update (fetch-then-merge preserves unchanged fields) |
280
+ | `delete_scheduled_transaction` | Write tool: delete a scheduled transaction |
256
281
 
257
282
  **Supported frequencies:** `never`, `daily`, `weekly`, `everyOtherWeek`, `twiceAMonth`, `every4Weeks`, `monthly`, `everyOtherMonth`, `every3Months`, `every4Months`, `twiceAYear`, `yearly`, `everyOtherYear`
258
283
 
@@ -260,7 +285,42 @@ That's it. Your AI can now talk to YNAB.
260
285
 
261
286
  | Tool | Description |
262
287
  |------|-------------|
263
- | `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. Includes a warning against blind approval. |
288
+ | `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. 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. |
289
+ | `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. |
290
+
291
+ ---
292
+
293
+ ## Workflow Safety Notes
294
+
295
+ ### Write Tool Opt-In
296
+
297
+ 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.
298
+
299
+ 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`.
300
+
301
+ ### Batch Updates
302
+
303
+ 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.
304
+
305
+ `update_transactions` now protects this path by refetching every requested transaction after the bulk API call and comparing the persisted fields with the requested fields. If anything differs, it retries that transaction once through `update_transaction`. The response includes:
306
+
307
+ ```json
308
+ {
309
+ "verification": {
310
+ "checked": 1,
311
+ "retried": [],
312
+ "failed": []
313
+ }
314
+ }
315
+ ```
316
+
317
+ Treat any `failed` entry as a real write failure and inspect the named transaction with `get_transaction`.
318
+
319
+ ### Credit Card Payment Transfers
320
+
321
+ 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.
322
+
323
+ 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.
264
324
 
265
325
  ---
266
326
 
@@ -268,11 +328,17 @@ That's it. Your AI can now talk to YNAB.
268
328
 
269
329
  | Variable | Required | Description |
270
330
  |----------|----------|-------------|
271
- | `YNAB_API_TOKEN` | Yes* | [Personal access token](https://app.ynab.com/settings/developer) from YNAB Developer Settings |
331
+ | `YNAB_API_TOKEN` | Yes* | [Personal access token](https://app.ynab.com/settings/developer) from YNAB Developer Settings. |
332
+ | `YNAB_API_TOKEN_FILE` | No | Path to a file containing the token. The file must be 4 KB or smaller. Used only when `YNAB_API_TOKEN` is unset. |
272
333
  | `YNAB_BUDGET_ID` | No | Default budget ID. If omitted, uses `"last-used"` (your most recently accessed budget). Run `list_budgets` to find IDs. |
334
+ | `YNAB_ALLOW_WRITES` | No | Set to `1` to register write tools. Any other value keeps the server read-only. |
273
335
  | `YNAB_OP_PATH` | No | 1Password secret reference for your API token (see below). Required only if using the 1Password fallback instead of `YNAB_API_TOKEN`. |
336
+ | `YNAB_RATE_LIMIT_PER_HOUR` | No | Client-side rate limiter. Defaults to `190`; set to `0` to disable for controlled tests. |
337
+ | `YNAB_RATE_LIMIT_BURST` | No | Maximum burst size before rate limiting pauses requests. Defaults to `10`. |
338
+ | `YNAB_HTTP_TIMEOUT_MS` | No | Per-request timeout. Defaults to `30000`. |
339
+ | `YNAB_MAX_RESPONSE_BYTES` | No | Maximum direct-fetch response size for newer endpoints. Defaults to `8388608`. |
274
340
 
275
- *`YNAB_API_TOKEN` is required unless `YNAB_OP_PATH` is set.
341
+ *`YNAB_API_TOKEN` is required unless `YNAB_API_TOKEN_FILE` or `YNAB_OP_PATH` is set.
276
342
 
277
343
  ### 1Password Integration
278
344
 
@@ -311,7 +377,9 @@ All amounts in tool inputs and outputs are in **dollars** (e.g., `-12.34` for a
311
377
 
312
378
  ## Rate Limiting
313
379
 
314
- The YNAB API allows **200 requests per hour** per access token, enforced on a rolling window. Each tool call typically uses one API request (except `update_scheduled_transaction` which uses two - a GET to fetch current state, then a PUT to merge changes). The server surfaces rate limit errors as standard MCP error responses.
380
+ 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. Each tool call typically uses one API request, except tools that deliberately verify or merge writes (`update_transactions`, `update_scheduled_transaction`) which perform additional reads.
381
+
382
+ 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.
315
383
 
316
384
  ---
317
385
 
@@ -326,11 +394,14 @@ The YNAB API allows **200 requests per hour** per access token, enforced on a ro
326
394
  ```
327
395
 
328
396
  - **Transport:** stdio (standard MCP server pattern)
329
- - **Auth:** Bearer token via `YNAB_API_TOKEN` environment variable
397
+ - **Auth:** Bearer token via `YNAB_API_TOKEN`, `YNAB_API_TOKEN_FILE`, or `YNAB_OP_PATH`
330
398
  - **SDK:** Official [`ynab`](https://www.npmjs.com/package/ynab) v2.5+ for core endpoints, direct `fetch` for newer API features
399
+ - **Safety:** read-only default, explicit write opt-in, host-pinned HTTPS requests to `api.ynab.com`, no redirect following, redacted token errors
331
400
  - **Validation:** All parameters validated with [Zod](https://zod.dev) schemas
332
401
  - **Error handling:** API errors are caught, formatted, and returned as MCP error responses with detail messages
333
402
 
403
+ For a hosted OAuth connector design, see [docs/hosted-oauth-connector.md](docs/hosted-oauth-connector.md).
404
+
334
405
  ---
335
406
 
336
407
  ## Testing
@@ -345,6 +416,26 @@ Use `YNAB_TEST_BUDGET_ID` to target a dedicated test budget without changing you
345
416
 
346
417
  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.
347
418
 
419
+ ### MCP Smoke Tests
420
+
421
+ 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, and require `YNAB_API_TOKEN` because this server validates credentials at startup.
422
+
423
+ ```bash
424
+ YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:list-tools
425
+ YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:review-unapproved
426
+ YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id YNAB_ALLOW_WRITES=1 npm run smoke:batch-verify
427
+ ```
428
+
429
+ To test the package currently published to npm instead of the local checkout:
430
+
431
+ ```bash
432
+ YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:list-tools -- --published
433
+ YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm run smoke:review-unapproved -- --published
434
+ YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id YNAB_ALLOW_WRITES=1 npm run smoke:batch-verify -- --published
435
+ ```
436
+
437
+ `smoke:list-tools` verifies that high-value read tools such as `review_unapproved`, `get_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.
438
+
348
439
  ---
349
440
 
350
441
  ## Development
@@ -363,6 +454,18 @@ YNAB_API_TOKEN=your-token npm start
363
454
 
364
455
  Zero additional dependencies. No build step. Pure ESM.
365
456
 
457
+ ### Release Checks
458
+
459
+ Before publishing, run:
460
+
461
+ ```bash
462
+ npm run release:check
463
+ npm run build:mcpb
464
+ npm pack --dry-run
465
+ ```
466
+
467
+ After publishing, run `npm run release:check:registry` to verify the npm `latest` dist-tag, repo metadata, README release links, and MCPB artifact references all agree on the same version.
468
+
366
469
  ---
367
470
 
368
471
  ## License
Binary file
@@ -0,0 +1,85 @@
1
+ # Hosted OAuth Connector Pattern
2
+
3
+ This package is a local stdio MCP server. A hosted connector should not ask users for a YNAB personal access token. It should follow an OAuth authorization-code flow, store per-user YNAB tokens server-side, and expose MCP over HTTPS.
4
+
5
+ ## Target Architecture
6
+
7
+ Use a Cloudflare Worker or equivalent edge service with these routes:
8
+
9
+ | Route | Purpose |
10
+ |---|---|
11
+ | `/mcp` | MCP endpoint served by the hosted connector. Requires a valid connector grant. |
12
+ | `/authorize` | Starts the connector OAuth flow and renders consent. |
13
+ | `/callback` | Receives the YNAB authorization code and completes the connector authorization. |
14
+ | `/token` | Issues connector access tokens to MCP hosts. |
15
+ | `/register` | Dynamic client registration, if the host supports it. |
16
+ | `/privacy` | Public privacy policy. |
17
+ | `/delete` | User-facing data deletion and grant revocation flow. |
18
+
19
+ The Smirnovlabs-style Cloudflare pattern is a good fit: wrap the MCP handler in an OAuth provider, use a stateful MCP agent for authenticated sessions, and keep the YNAB OAuth token refresh path separate from tool registration. Composio-style hosted connectors use the same core shape: hosted OAuth, per-user token vaulting, and a remote MCP endpoint instead of user-supplied PATs.
20
+
21
+ ## YNAB OAuth Requirements
22
+
23
+ Use YNAB OAuth instead of personal access tokens:
24
+
25
+ - Authorization URL: `https://app.ynab.com/oauth/authorize`
26
+ - Token URL: `https://app.ynab.com/oauth/token`
27
+ - Response type: `code`
28
+ - PKCE: `S256` code challenge and verifier
29
+ - Scope: use the minimum viable scope. Prefer read-only unless the hosted connector explicitly offers write tools.
30
+
31
+ Required hosted environment values:
32
+
33
+ | Variable | Purpose |
34
+ |---|---|
35
+ | `YNAB_OAUTH_CLIENT_ID` | YNAB OAuth application client ID. |
36
+ | `YNAB_OAUTH_CLIENT_SECRET` | YNAB OAuth application secret. Store only in the host secret manager. |
37
+ | `YNAB_OAUTH_SCOPE` | Requested YNAB OAuth scope. |
38
+ | `CONNECTOR_BASE_URL` | Public connector origin, for redirect URL construction. |
39
+ | `TOKEN_KV` | Durable KV/DB binding for encrypted YNAB access and refresh tokens. |
40
+ | `OAUTH_STATE_KV` | Short-lived state storage for authorization requests. |
41
+
42
+ ## Safety Model
43
+
44
+ Carry the local safety model into the hosted connector:
45
+
46
+ 1. Register read tools by default.
47
+ 2. Register write tools only when the connector grant, deployment config, and requested YNAB scope all allow writes.
48
+ 3. Annotate read tools with `readOnlyHint: true`.
49
+ 4. Annotate write tools with `readOnlyHint: false`, and use `destructiveHint: true` for delete tools.
50
+ 5. Pin outbound YNAB HTTP traffic to `https://api.ynab.com`; reject redirects and non-YNAB hosts.
51
+ 6. Redact `Authorization`, bearer tokens, access tokens, and refresh tokens from logs and MCP errors.
52
+ 7. Add a delete-data flow that revokes connector grants and removes stored YNAB tokens.
53
+
54
+ ## OAuth State Handling
55
+
56
+ The authorization flow should bind state to both server-side storage and a browser cookie:
57
+
58
+ 1. Generate a random OAuth `state`.
59
+ 2. Generate a PKCE verifier and `S256` challenge.
60
+ 3. Store `{ state, verifier, redirect_uri, client_id, scope }` with a 10-minute TTL.
61
+ 4. Set an HttpOnly, Secure, SameSite=Lax cookie containing a SHA-256 hash of `state`.
62
+ 5. On `/callback`, require the query `state`, the stored state record, and the cookie hash to match.
63
+ 6. Delete the state record after first use.
64
+
65
+ This prevents replay and cross-tab confusion while keeping the YNAB token exchange server-side.
66
+
67
+ ## Token Lifecycle
68
+
69
+ The hosted connector should store tokens by connector user or YNAB user ID:
70
+
71
+ - On callback, exchange the code for YNAB access and refresh tokens.
72
+ - Fetch the YNAB user profile immediately and persist the YNAB user ID with the token record.
73
+ - Refresh tokens before expiry, with a small safety window such as 60 seconds.
74
+ - If refresh fails, delete the stale token record and require reauthorization.
75
+ - Never expose YNAB access or refresh tokens to the MCP host or model context.
76
+
77
+ ## Deployment Checklist
78
+
79
+ - Create and verify a YNAB OAuth application with the production redirect URI.
80
+ - Decide whether the hosted connector offers read-only only, read/write, or separate read-only and write-enabled variants.
81
+ - Publish privacy and deletion pages before public distribution.
82
+ - Add non-affiliation language; this connector is not an official YNAB product.
83
+ - Run read-only smoke tests through `/mcp`.
84
+ - If writes are enabled, run the batch category+approval smoke against a dedicated test budget and assert post-write refetch verification.
85
+