@oliverames/ynab-mcp-server 1.3.0 → 1.4.0
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 +23 -19
- package/index.js +244 -143
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<code>44 tools</code> •
|
|
14
14
|
<code>100% API coverage</code> •
|
|
15
|
-
<code>YNAB API v1.
|
|
15
|
+
<code>YNAB API v1.83</code>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<p align="center">
|
|
25
25
|
<a href="#quick-start">Quick Start</a> •
|
|
26
26
|
<a href="#what-you-can-do">What You Can Do</a> •
|
|
27
|
-
<a href="#tools-reference">All
|
|
27
|
+
<a href="#tools-reference">All 44 Tools</a> •
|
|
28
28
|
<a href="#environment-variables">Configuration</a>
|
|
29
29
|
</p>
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
## Why This Exists
|
|
34
34
|
|
|
35
|
-
YNAB's budgeting philosophy works best when you interact with your budget frequently
|
|
35
|
+
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.
|
|
36
36
|
|
|
37
37
|
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.
|
|
38
38
|
|
|
@@ -121,7 +121,7 @@ That's it. Your AI can now talk to YNAB.
|
|
|
121
121
|
|
|
122
122
|
## Features
|
|
123
123
|
|
|
124
|
-
**Complete YNAB API v1.
|
|
124
|
+
**Complete YNAB API v1.83 coverage** with 44 tools:
|
|
125
125
|
|
|
126
126
|
| Resource | Tools | Capabilities |
|
|
127
127
|
|----------|-------|-------------|
|
|
@@ -138,15 +138,17 @@ That's it. Your AI can now talk to YNAB.
|
|
|
138
138
|
|
|
139
139
|
### Design Decisions
|
|
140
140
|
|
|
141
|
-
- **Dollar amounts everywhere**
|
|
142
|
-
- **Smart budget resolution**
|
|
143
|
-
- **Split transactions**
|
|
144
|
-
- **Bulk operations**
|
|
145
|
-
- **Fetch-then-merge updates**
|
|
146
|
-
- **Fuzzy search**
|
|
147
|
-
- **Approval workflow**
|
|
148
|
-
- **Nullable updates**
|
|
149
|
-
- **
|
|
141
|
+
- **Dollar amounts everywhere** - inputs and outputs are in dollars (`-12.34`), never milliunits (`-12340`). Conversion is automatic and transparent.
|
|
142
|
+
- **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.
|
|
143
|
+
- **Split transactions** - first-class support for subtransactions in create, read, and format operations.
|
|
144
|
+
- **Bulk operations** - `create_transactions` and `update_transactions` handle arrays in a single API call.
|
|
145
|
+
- **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.
|
|
146
|
+
- **Fuzzy search** - `search_categories` and `search_payees` do case-insensitive partial matching across all entries.
|
|
147
|
+
- **Approval workflow** - `review_unapproved` groups transactions into "ready to approve" (categorized, split, or transfer) and "needs attention" (uncategorized), with a built-in warning against approving uncategorized entries.
|
|
148
|
+
- **Nullable updates** - update tools accept `null` for clearable fields (`memo`, `payeeName`, `categoryId`, `flagColor`) to distinguish "don't change" (omit) from "clear this field" (`null`).
|
|
149
|
+
- **Target behavior support** - category create/update tools expose `goalNeedsWholeAmount` for YNAB's "Set aside another" vs. "Refill up to" goal behavior.
|
|
150
|
+
- **Delta request support** - high-volume list tools accept `lastKnowledgeOfServer` and return `server_knowledge` when that parameter is provided.
|
|
151
|
+
- **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).
|
|
150
152
|
|
|
151
153
|
---
|
|
152
154
|
|
|
@@ -227,7 +229,7 @@ That's it. Your AI can now talk to YNAB.
|
|
|
227
229
|
| `get_transaction` | Get a single transaction by ID (includes subtransactions) |
|
|
228
230
|
| `create_transaction` | Create a transaction with optional split (subtransactions must sum to total) |
|
|
229
231
|
| `create_transactions` | Bulk create multiple transactions in a single API call (supports split transactions) |
|
|
230
|
-
| `update_transaction` | Partial update
|
|
232
|
+
| `update_transaction` | Partial update - only specified fields change |
|
|
231
233
|
| `update_transactions` | Batch update multiple transactions at once |
|
|
232
234
|
| `delete_transaction` | Delete a transaction |
|
|
233
235
|
| `import_transactions` | Trigger import from linked bank accounts |
|
|
@@ -299,7 +301,7 @@ All amounts in tool inputs and outputs are in **dollars** (e.g., `-12.34` for a
|
|
|
299
301
|
|
|
300
302
|
## Rate Limiting
|
|
301
303
|
|
|
302
|
-
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
|
|
304
|
+
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.
|
|
303
305
|
|
|
304
306
|
---
|
|
305
307
|
|
|
@@ -323,13 +325,15 @@ The YNAB API allows **200 requests per hour** per access token, enforced on a ro
|
|
|
323
325
|
|
|
324
326
|
## Testing
|
|
325
327
|
|
|
326
|
-
The test suite
|
|
328
|
+
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.
|
|
327
329
|
|
|
328
330
|
```bash
|
|
329
331
|
YNAB_API_TOKEN=your-token YNAB_BUDGET_ID=your-budget-id npm test
|
|
330
332
|
```
|
|
331
333
|
|
|
332
|
-
|
|
334
|
+
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`.
|
|
335
|
+
|
|
336
|
+
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.
|
|
333
337
|
|
|
334
338
|
---
|
|
335
339
|
|
|
@@ -344,8 +348,8 @@ YNAB_API_TOKEN=your-token npm start
|
|
|
344
348
|
|
|
345
349
|
### Dependencies
|
|
346
350
|
|
|
347
|
-
- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
|
|
348
|
-
- [`ynab`](https://www.npmjs.com/package/ynab)
|
|
351
|
+
- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk) - MCP server framework
|
|
352
|
+
- [`ynab`](https://www.npmjs.com/package/ynab) - Official YNAB JavaScript client
|
|
349
353
|
|
|
350
354
|
Zero additional dependencies. No build step. Pure ESM.
|
|
351
355
|
|
package/index.js
CHANGED
|
@@ -9,18 +9,22 @@ import * as ynab from "ynab";
|
|
|
9
9
|
// --- Init ---
|
|
10
10
|
|
|
11
11
|
let API_TOKEN = process.env.YNAB_API_TOKEN;
|
|
12
|
+
let opLookupError;
|
|
12
13
|
if (!API_TOKEN && process.env.YNAB_OP_PATH) {
|
|
13
14
|
try {
|
|
14
15
|
API_TOKEN = execFileSync(
|
|
15
16
|
"op", ["read", process.env.YNAB_OP_PATH],
|
|
16
17
|
{ encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }
|
|
17
18
|
).trim();
|
|
18
|
-
} catch {
|
|
19
|
-
|
|
19
|
+
} catch (e) {
|
|
20
|
+
opLookupError = e.stderr?.toString().trim() || e.message || "unknown 1Password CLI error";
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
if (!API_TOKEN) {
|
|
23
|
-
|
|
24
|
+
const opMessage = process.env.YNAB_OP_PATH
|
|
25
|
+
? ` Could not read YNAB_OP_PATH via 1Password CLI: ${opLookupError}.`
|
|
26
|
+
: " Set YNAB_OP_PATH to enable 1Password CLI fallback (e.g. op://Vault/Item/credential).";
|
|
27
|
+
console.error(`YNAB_API_TOKEN environment variable is required.${opMessage}`);
|
|
24
28
|
process.exit(1);
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -45,6 +49,16 @@ function dollarsMap(obj) {
|
|
|
45
49
|
return obj ? Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, dollars(v)])) : obj;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
function withCurrencyFields(out, source, fields) {
|
|
53
|
+
for (const field of fields) {
|
|
54
|
+
const formatted = `${field}_formatted`;
|
|
55
|
+
const currency = `${field}_currency`;
|
|
56
|
+
if (formatted in source) out[formatted] = source[formatted];
|
|
57
|
+
if (currency in source) out[currency] = source[currency];
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
function mapTransactionInput(t) {
|
|
49
63
|
const out = {
|
|
50
64
|
account_id: t.accountId,
|
|
@@ -71,7 +85,7 @@ function mapTransactionInput(t) {
|
|
|
71
85
|
return out;
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
// Sparse patch mapper for update_transaction / update_transactions
|
|
88
|
+
// Sparse patch mapper for update_transaction / update_transactions - only includes fields that were explicitly provided
|
|
75
89
|
function mapTransactionUpdate(t) {
|
|
76
90
|
const out = {};
|
|
77
91
|
if (t.accountId !== undefined) out.account_id = t.accountId;
|
|
@@ -91,6 +105,12 @@ function ok(data) {
|
|
|
91
105
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
function collection(data, key, items, lastKnowledgeOfServer) {
|
|
109
|
+
return lastKnowledgeOfServer === undefined
|
|
110
|
+
? items
|
|
111
|
+
: { [key]: items, server_knowledge: data.server_knowledge };
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
async function run(fn) {
|
|
95
115
|
try {
|
|
96
116
|
return await fn();
|
|
@@ -106,14 +126,19 @@ async function run(fn) {
|
|
|
106
126
|
|
|
107
127
|
// Direct API helper for endpoints not yet in the ynab SDK
|
|
108
128
|
const BASE_URL = "https://api.ynab.com/v1";
|
|
109
|
-
async function ynabFetch(path, { method = "GET", body } = {}) {
|
|
129
|
+
async function ynabFetch(path, { method = "GET", body, query } = {}) {
|
|
130
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
131
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
132
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
133
|
+
}
|
|
110
134
|
const opts = {
|
|
111
135
|
method,
|
|
112
136
|
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
|
|
113
137
|
};
|
|
114
138
|
if (body) opts.body = JSON.stringify(body);
|
|
115
|
-
const res = await fetch(
|
|
116
|
-
const
|
|
139
|
+
const res = await fetch(url, opts);
|
|
140
|
+
const text = await res.text();
|
|
141
|
+
const json = text ? JSON.parse(text) : {};
|
|
117
142
|
if (!res.ok) {
|
|
118
143
|
const err = new Error(json?.error?.detail || `HTTP ${res.status}`);
|
|
119
144
|
err.error = json?.error;
|
|
@@ -212,16 +237,20 @@ function formatAccount(a) {
|
|
|
212
237
|
deleted: a.deleted,
|
|
213
238
|
};
|
|
214
239
|
if ("note" in a) out.note = a.note;
|
|
215
|
-
return out;
|
|
240
|
+
return withCurrencyFields(out, a, ["balance", "cleared_balance", "uncleared_balance"]);
|
|
216
241
|
}
|
|
217
242
|
|
|
218
243
|
server.registerTool(
|
|
219
244
|
"list_accounts",
|
|
220
|
-
{ description: "List all accounts in a budget", inputSchema: {
|
|
221
|
-
|
|
245
|
+
{ description: "List all accounts in a budget", inputSchema: {
|
|
246
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
247
|
+
lastKnowledgeOfServer: z.number().int().nonnegative().optional().describe("Delta request server knowledge. When provided, returns { accounts, server_knowledge }."),
|
|
248
|
+
} },
|
|
249
|
+
({ budgetId, lastKnowledgeOfServer }) =>
|
|
222
250
|
run(async () => {
|
|
223
|
-
const { data } = await api.accounts.getAccounts(resolveBudgetId(budgetId));
|
|
224
|
-
|
|
251
|
+
const { data } = await api.accounts.getAccounts(resolveBudgetId(budgetId), lastKnowledgeOfServer);
|
|
252
|
+
const accounts = data.accounts.map(formatAccount);
|
|
253
|
+
return ok(collection(data, "accounts", accounts, lastKnowledgeOfServer));
|
|
225
254
|
})
|
|
226
255
|
);
|
|
227
256
|
|
|
@@ -258,7 +287,7 @@ server.registerTool(
|
|
|
258
287
|
// ==================== Categories ====================
|
|
259
288
|
|
|
260
289
|
function formatCategory(c) {
|
|
261
|
-
|
|
290
|
+
const out = {
|
|
262
291
|
id: c.id,
|
|
263
292
|
category_group_id: c.category_group_id,
|
|
264
293
|
category_group_name: c.category_group_name,
|
|
@@ -275,6 +304,7 @@ function formatCategory(c) {
|
|
|
275
304
|
goal_cadence_frequency: c.goal_cadence_frequency,
|
|
276
305
|
goal_creation_month: c.goal_creation_month,
|
|
277
306
|
goal_target: dollars(c.goal_target),
|
|
307
|
+
goal_target_month: c.goal_target_month,
|
|
278
308
|
goal_target_date: c.goal_target_date,
|
|
279
309
|
goal_percentage_complete: c.goal_percentage_complete,
|
|
280
310
|
goal_months_to_budget: c.goal_months_to_budget,
|
|
@@ -282,34 +312,53 @@ function formatCategory(c) {
|
|
|
282
312
|
goal_overall_funded: dollars(c.goal_overall_funded),
|
|
283
313
|
goal_overall_left: dollars(c.goal_overall_left),
|
|
284
314
|
goal_needs_whole_amount: c.goal_needs_whole_amount,
|
|
315
|
+
goal_snoozed_at: c.goal_snoozed_at,
|
|
285
316
|
deleted: c.deleted,
|
|
286
317
|
};
|
|
318
|
+
return withCurrencyFields(out, c, [
|
|
319
|
+
"budgeted",
|
|
320
|
+
"activity",
|
|
321
|
+
"balance",
|
|
322
|
+
"goal_target",
|
|
323
|
+
"goal_under_funded",
|
|
324
|
+
"goal_overall_funded",
|
|
325
|
+
"goal_overall_left",
|
|
326
|
+
]);
|
|
287
327
|
}
|
|
288
328
|
|
|
289
329
|
server.registerTool(
|
|
290
330
|
"list_categories",
|
|
291
|
-
{ description: "List all category groups and their categories", inputSchema: {
|
|
292
|
-
|
|
331
|
+
{ description: "List all category groups and their categories", inputSchema: {
|
|
332
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
333
|
+
lastKnowledgeOfServer: z.number().int().nonnegative().optional().describe("Delta request server knowledge. When provided, returns { category_groups, server_knowledge }."),
|
|
334
|
+
} },
|
|
335
|
+
({ budgetId, lastKnowledgeOfServer }) =>
|
|
293
336
|
run(async () => {
|
|
294
|
-
const { data } = await api.categories.getCategories(resolveBudgetId(budgetId));
|
|
295
|
-
|
|
296
|
-
data.category_groups.map((g) => ({
|
|
337
|
+
const { data } = await api.categories.getCategories(resolveBudgetId(budgetId), lastKnowledgeOfServer);
|
|
338
|
+
const categoryGroups = data.category_groups.map((g) => ({
|
|
297
339
|
id: g.id,
|
|
298
340
|
name: g.name,
|
|
299
341
|
hidden: g.hidden,
|
|
300
342
|
deleted: g.deleted,
|
|
301
|
-
categories: g.categories.map((c) =>
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
343
|
+
categories: g.categories.map((c) =>
|
|
344
|
+
withCurrencyFields(
|
|
345
|
+
{
|
|
346
|
+
id: c.id,
|
|
347
|
+
name: c.name,
|
|
348
|
+
hidden: c.hidden,
|
|
349
|
+
budgeted: dollars(c.budgeted),
|
|
350
|
+
activity: dollars(c.activity),
|
|
351
|
+
balance: dollars(c.balance),
|
|
352
|
+
goal_type: c.goal_type,
|
|
353
|
+
goal_needs_whole_amount: c.goal_needs_whole_amount,
|
|
354
|
+
deleted: c.deleted,
|
|
355
|
+
},
|
|
356
|
+
c,
|
|
357
|
+
["budgeted", "activity", "balance"]
|
|
358
|
+
)
|
|
359
|
+
),
|
|
360
|
+
}));
|
|
361
|
+
return ok(collection(data, "category_groups", categoryGroups, lastKnowledgeOfServer));
|
|
313
362
|
})
|
|
314
363
|
);
|
|
315
364
|
|
|
@@ -367,8 +416,9 @@ server.registerTool(
|
|
|
367
416
|
categoryGroupId: z.string().optional().describe("Move to a different category group"),
|
|
368
417
|
goalTarget: z.number().nullable().optional().describe("Goal target amount in dollars (only if category already has a goal)"),
|
|
369
418
|
goalTargetDate: z.string().nullable().optional().describe("Goal target date in ISO format (e.g. 2026-12-01, null to clear)"),
|
|
419
|
+
goalNeedsWholeAmount: z.boolean().nullable().optional().describe("For NEED goals, true uses 'Set aside another' behavior and false uses 'Refill up to' behavior"),
|
|
370
420
|
} },
|
|
371
|
-
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget, goalTargetDate }) =>
|
|
421
|
+
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget, goalTargetDate, goalNeedsWholeAmount }) =>
|
|
372
422
|
run(async () => {
|
|
373
423
|
const cat = {};
|
|
374
424
|
if (name !== undefined) cat.name = name;
|
|
@@ -376,9 +426,11 @@ server.registerTool(
|
|
|
376
426
|
if (categoryGroupId !== undefined) cat.category_group_id = categoryGroupId;
|
|
377
427
|
if (goalTarget !== undefined) cat.goal_target = goalTarget != null ? milliunits(goalTarget) : null;
|
|
378
428
|
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
429
|
+
if (goalNeedsWholeAmount !== undefined) cat.goal_needs_whole_amount = goalNeedsWholeAmount;
|
|
379
430
|
|
|
380
|
-
const
|
|
381
|
-
|
|
431
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/categories/${categoryId}`, {
|
|
432
|
+
method: "PATCH",
|
|
433
|
+
body: { category: cat },
|
|
382
434
|
});
|
|
383
435
|
return ok(formatCategory(data.category));
|
|
384
436
|
})
|
|
@@ -393,15 +445,17 @@ server.registerTool(
|
|
|
393
445
|
note: z.string().optional().describe("Category note"),
|
|
394
446
|
goalTarget: z.number().optional().describe("Goal target amount in dollars (creates a 'Needed for Spending' goal)"),
|
|
395
447
|
goalTargetDate: z.string().optional().describe("Goal target date in ISO format (e.g. 2026-12-01)"),
|
|
448
|
+
goalNeedsWholeAmount: z.boolean().optional().describe("For NEED goals, true uses 'Set aside another' behavior and false uses 'Refill up to' behavior"),
|
|
396
449
|
} },
|
|
397
|
-
({ budgetId, categoryGroupId, name, note, goalTarget, goalTargetDate }) =>
|
|
450
|
+
({ budgetId, categoryGroupId, name, note, goalTarget, goalTargetDate, goalNeedsWholeAmount }) =>
|
|
398
451
|
run(async () => {
|
|
399
452
|
const bid = resolveBudgetId(budgetId);
|
|
400
453
|
const cat = { category_group_id: categoryGroupId, name };
|
|
401
454
|
if (note !== undefined) cat.note = note;
|
|
402
455
|
if (goalTarget !== undefined) cat.goal_target = milliunits(goalTarget);
|
|
403
456
|
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
404
|
-
|
|
457
|
+
if (goalNeedsWholeAmount !== undefined) cat.goal_needs_whole_amount = goalNeedsWholeAmount;
|
|
458
|
+
const data = await ynabFetch(`/plans/${bid}/categories`, {
|
|
405
459
|
method: "POST",
|
|
406
460
|
body: { category: cat },
|
|
407
461
|
});
|
|
@@ -417,7 +471,7 @@ server.registerTool(
|
|
|
417
471
|
} },
|
|
418
472
|
({ budgetId, name }) =>
|
|
419
473
|
run(async () => {
|
|
420
|
-
const data = await ynabFetch(`/
|
|
474
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/category_groups`, {
|
|
421
475
|
method: "POST",
|
|
422
476
|
body: { category_group: { name } },
|
|
423
477
|
});
|
|
@@ -434,7 +488,7 @@ server.registerTool(
|
|
|
434
488
|
} },
|
|
435
489
|
({ budgetId, categoryGroupId, name }) =>
|
|
436
490
|
run(async () => {
|
|
437
|
-
const data = await ynabFetch(`/
|
|
491
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/category_groups/${categoryGroupId}`, {
|
|
438
492
|
method: "PATCH",
|
|
439
493
|
body: { category_group: { name } },
|
|
440
494
|
});
|
|
@@ -446,11 +500,15 @@ server.registerTool(
|
|
|
446
500
|
|
|
447
501
|
server.registerTool(
|
|
448
502
|
"list_payees",
|
|
449
|
-
{ description: "List all payees", inputSchema: {
|
|
450
|
-
|
|
503
|
+
{ description: "List all payees", inputSchema: {
|
|
504
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
505
|
+
lastKnowledgeOfServer: z.number().int().nonnegative().optional().describe("Delta request server knowledge. When provided, returns { payees, server_knowledge }."),
|
|
506
|
+
} },
|
|
507
|
+
({ budgetId, lastKnowledgeOfServer }) =>
|
|
451
508
|
run(async () => {
|
|
452
|
-
const { data } = await api.payees.getPayees(resolveBudgetId(budgetId));
|
|
453
|
-
|
|
509
|
+
const { data } = await api.payees.getPayees(resolveBudgetId(budgetId), lastKnowledgeOfServer);
|
|
510
|
+
const payees = data.payees.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted }));
|
|
511
|
+
return ok(collection(data, "payees", payees, lastKnowledgeOfServer));
|
|
454
512
|
})
|
|
455
513
|
);
|
|
456
514
|
|
|
@@ -491,7 +549,7 @@ server.registerTool(
|
|
|
491
549
|
} },
|
|
492
550
|
({ budgetId, name }) =>
|
|
493
551
|
run(async () => {
|
|
494
|
-
const data = await ynabFetch(`/
|
|
552
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/payees`, {
|
|
495
553
|
method: "POST",
|
|
496
554
|
body: { payee: { name } },
|
|
497
555
|
});
|
|
@@ -541,22 +599,30 @@ server.registerTool(
|
|
|
541
599
|
|
|
542
600
|
server.registerTool(
|
|
543
601
|
"list_months",
|
|
544
|
-
{ description: "List all budget months", inputSchema: {
|
|
545
|
-
|
|
602
|
+
{ description: "List all budget months", inputSchema: {
|
|
603
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
604
|
+
lastKnowledgeOfServer: z.number().int().nonnegative().optional().describe("Delta request server knowledge. When provided, returns { months, server_knowledge }."),
|
|
605
|
+
} },
|
|
606
|
+
({ budgetId, lastKnowledgeOfServer }) =>
|
|
546
607
|
run(async () => {
|
|
547
|
-
const { data } = await api.months.getBudgetMonths(resolveBudgetId(budgetId));
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
608
|
+
const { data } = await api.months.getBudgetMonths(resolveBudgetId(budgetId), lastKnowledgeOfServer);
|
|
609
|
+
const months = data.months.map((m) =>
|
|
610
|
+
withCurrencyFields(
|
|
611
|
+
{
|
|
612
|
+
month: m.month,
|
|
613
|
+
note: m.note,
|
|
614
|
+
income: dollars(m.income),
|
|
615
|
+
budgeted: dollars(m.budgeted),
|
|
616
|
+
activity: dollars(m.activity),
|
|
617
|
+
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
618
|
+
age_of_money: m.age_of_money,
|
|
619
|
+
deleted: m.deleted,
|
|
620
|
+
},
|
|
621
|
+
m,
|
|
622
|
+
["income", "budgeted", "activity", "to_be_budgeted"]
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
return ok(collection(data, "months", months, lastKnowledgeOfServer));
|
|
560
626
|
})
|
|
561
627
|
);
|
|
562
628
|
|
|
@@ -570,7 +636,7 @@ server.registerTool(
|
|
|
570
636
|
run(async () => {
|
|
571
637
|
const { data } = await api.months.getBudgetMonth(resolveBudgetId(budgetId), month);
|
|
572
638
|
const m = data.month;
|
|
573
|
-
|
|
639
|
+
const out = {
|
|
574
640
|
month: m.month,
|
|
575
641
|
note: m.note,
|
|
576
642
|
income: dollars(m.income),
|
|
@@ -579,27 +645,38 @@ server.registerTool(
|
|
|
579
645
|
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
580
646
|
age_of_money: m.age_of_money,
|
|
581
647
|
deleted: m.deleted,
|
|
582
|
-
categories: m.categories?.map((c) =>
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
648
|
+
categories: m.categories?.map((c) =>
|
|
649
|
+
withCurrencyFields(
|
|
650
|
+
{
|
|
651
|
+
id: c.id,
|
|
652
|
+
name: c.name,
|
|
653
|
+
hidden: c.hidden,
|
|
654
|
+
category_group_name: c.category_group_name,
|
|
655
|
+
budgeted: dollars(c.budgeted),
|
|
656
|
+
activity: dollars(c.activity),
|
|
657
|
+
balance: dollars(c.balance),
|
|
658
|
+
goal_type: c.goal_type,
|
|
659
|
+
goal_needs_whole_amount: c.goal_needs_whole_amount,
|
|
660
|
+
goal_target: dollars(c.goal_target),
|
|
661
|
+
goal_target_month: c.goal_target_month,
|
|
662
|
+
goal_target_date: c.goal_target_date,
|
|
663
|
+
goal_under_funded: dollars(c.goal_under_funded),
|
|
664
|
+
goal_snoozed_at: c.goal_snoozed_at,
|
|
665
|
+
deleted: c.deleted,
|
|
666
|
+
},
|
|
667
|
+
c,
|
|
668
|
+
["budgeted", "activity", "balance", "goal_target", "goal_under_funded"]
|
|
669
|
+
)
|
|
670
|
+
),
|
|
671
|
+
};
|
|
672
|
+
return ok(withCurrencyFields(out, m, ["income", "budgeted", "activity", "to_be_budgeted"]));
|
|
596
673
|
})
|
|
597
674
|
);
|
|
598
675
|
|
|
599
676
|
// ==================== Money Movements ====================
|
|
600
677
|
|
|
601
678
|
function formatMoneyMovement(m) {
|
|
602
|
-
return {
|
|
679
|
+
return withCurrencyFields({
|
|
603
680
|
id: m.id,
|
|
604
681
|
month: m.month,
|
|
605
682
|
moved_at: m.moved_at,
|
|
@@ -610,7 +687,7 @@ function formatMoneyMovement(m) {
|
|
|
610
687
|
to_category_id: m.to_category_id,
|
|
611
688
|
amount: dollars(m.amount),
|
|
612
689
|
deleted: m.deleted,
|
|
613
|
-
};
|
|
690
|
+
}, m, ["amount"]);
|
|
614
691
|
}
|
|
615
692
|
|
|
616
693
|
server.registerTool(
|
|
@@ -618,7 +695,7 @@ server.registerTool(
|
|
|
618
695
|
{ description: "List all money movements (budget re-allocations between categories)", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
619
696
|
({ budgetId }) =>
|
|
620
697
|
run(async () => {
|
|
621
|
-
const data = await ynabFetch(`/
|
|
698
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/money_movements`);
|
|
622
699
|
return ok(data.money_movements.map(formatMoneyMovement));
|
|
623
700
|
})
|
|
624
701
|
);
|
|
@@ -631,7 +708,7 @@ server.registerTool(
|
|
|
631
708
|
} },
|
|
632
709
|
({ budgetId, month }) =>
|
|
633
710
|
run(async () => {
|
|
634
|
-
const data = await ynabFetch(`/
|
|
711
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/months/${month}/money_movements`);
|
|
635
712
|
return ok(data.money_movements.map(formatMoneyMovement));
|
|
636
713
|
})
|
|
637
714
|
);
|
|
@@ -641,7 +718,7 @@ server.registerTool(
|
|
|
641
718
|
{ description: "List all money movement groups (batches of related money movements)", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
642
719
|
({ budgetId }) =>
|
|
643
720
|
run(async () => {
|
|
644
|
-
const data = await ynabFetch(`/
|
|
721
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/money_movement_groups`);
|
|
645
722
|
return ok(data.money_movement_groups);
|
|
646
723
|
})
|
|
647
724
|
);
|
|
@@ -654,7 +731,7 @@ server.registerTool(
|
|
|
654
731
|
} },
|
|
655
732
|
({ budgetId, month }) =>
|
|
656
733
|
run(async () => {
|
|
657
|
-
const data = await ynabFetch(`/
|
|
734
|
+
const data = await ynabFetch(`/plans/${resolveBudgetId(budgetId)}/months/${month}/money_movement_groups`);
|
|
658
735
|
return ok(data.money_movement_groups);
|
|
659
736
|
})
|
|
660
737
|
);
|
|
@@ -662,43 +739,50 @@ server.registerTool(
|
|
|
662
739
|
// ==================== Transactions ====================
|
|
663
740
|
|
|
664
741
|
function formatTransaction(t) {
|
|
665
|
-
|
|
742
|
+
const out = {
|
|
666
743
|
id: t.id,
|
|
667
744
|
date: t.date,
|
|
668
745
|
amount: dollars(t.amount),
|
|
669
|
-
memo: t.memo,
|
|
746
|
+
memo: t.memo ?? null,
|
|
670
747
|
cleared: t.cleared,
|
|
671
748
|
approved: t.approved,
|
|
672
|
-
flag_color: t.flag_color,
|
|
673
|
-
flag_name: t.flag_name,
|
|
749
|
+
flag_color: t.flag_color ?? null,
|
|
750
|
+
flag_name: t.flag_name ?? null,
|
|
674
751
|
account_id: t.account_id,
|
|
675
752
|
account_name: t.account_name,
|
|
676
|
-
payee_id: t.payee_id,
|
|
677
|
-
payee_name: t.payee_name,
|
|
678
|
-
category_id: t.category_id,
|
|
679
|
-
category_name: t.category_name,
|
|
680
|
-
transfer_account_id: t.transfer_account_id,
|
|
681
|
-
transfer_transaction_id: t.transfer_transaction_id,
|
|
682
|
-
matched_transaction_id: t.matched_transaction_id,
|
|
683
|
-
import_id: t.import_id,
|
|
684
|
-
import_payee_name: t.import_payee_name,
|
|
685
|
-
import_payee_name_original: t.import_payee_name_original,
|
|
686
|
-
debt_transaction_type: t.debt_transaction_type,
|
|
753
|
+
payee_id: t.payee_id ?? null,
|
|
754
|
+
payee_name: t.payee_name ?? null,
|
|
755
|
+
category_id: t.category_id ?? null,
|
|
756
|
+
category_name: t.category_name ?? null,
|
|
757
|
+
transfer_account_id: t.transfer_account_id ?? null,
|
|
758
|
+
transfer_transaction_id: t.transfer_transaction_id ?? null,
|
|
759
|
+
matched_transaction_id: t.matched_transaction_id ?? null,
|
|
760
|
+
import_id: t.import_id ?? null,
|
|
761
|
+
import_payee_name: t.import_payee_name ?? null,
|
|
762
|
+
import_payee_name_original: t.import_payee_name_original ?? null,
|
|
763
|
+
debt_transaction_type: t.debt_transaction_type ?? null,
|
|
687
764
|
deleted: t.deleted,
|
|
688
|
-
subtransactions: t.subtransactions?.map((s) =>
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
765
|
+
subtransactions: t.subtransactions?.map((s) =>
|
|
766
|
+
withCurrencyFields(
|
|
767
|
+
{
|
|
768
|
+
id: s.id,
|
|
769
|
+
transaction_id: s.transaction_id,
|
|
770
|
+
amount: dollars(s.amount),
|
|
771
|
+
memo: s.memo ?? null,
|
|
772
|
+
payee_id: s.payee_id ?? null,
|
|
773
|
+
payee_name: s.payee_name ?? null,
|
|
774
|
+
category_id: s.category_id ?? null,
|
|
775
|
+
category_name: s.category_name ?? null,
|
|
776
|
+
transfer_account_id: s.transfer_account_id ?? null,
|
|
777
|
+
transfer_transaction_id: s.transfer_transaction_id ?? null,
|
|
778
|
+
deleted: s.deleted,
|
|
779
|
+
},
|
|
780
|
+
s,
|
|
781
|
+
["amount"]
|
|
782
|
+
)
|
|
783
|
+
),
|
|
701
784
|
};
|
|
785
|
+
return withCurrencyFields(out, t, ["amount"]);
|
|
702
786
|
}
|
|
703
787
|
|
|
704
788
|
server.registerTool(
|
|
@@ -711,30 +795,36 @@ server.registerTool(
|
|
|
711
795
|
categoryId: z.string().optional().describe("Filter by category ID"),
|
|
712
796
|
payeeId: z.string().optional().describe("Filter by payee ID"),
|
|
713
797
|
month: z.string().optional().describe("Filter by month (YYYY-MM-DD, first of month)"),
|
|
798
|
+
lastKnowledgeOfServer: z.number().int().nonnegative().optional().describe("Delta request server knowledge. When provided, returns { transactions, server_knowledge }."),
|
|
714
799
|
} },
|
|
715
|
-
({ budgetId, sinceDate, type, accountId, categoryId, payeeId, month }) =>
|
|
800
|
+
({ budgetId, sinceDate, type, accountId, categoryId, payeeId, month, lastKnowledgeOfServer }) =>
|
|
716
801
|
run(async () => {
|
|
717
802
|
const bid = resolveBudgetId(budgetId);
|
|
718
803
|
let transactions;
|
|
804
|
+
let data;
|
|
805
|
+
const resourceFilters = [accountId, categoryId, payeeId, month].filter((value) => value !== undefined && value !== null && value !== "");
|
|
806
|
+
if (resourceFilters.length > 1) {
|
|
807
|
+
throw new Error("Provide only one of accountId, categoryId, payeeId, or month.");
|
|
808
|
+
}
|
|
719
809
|
|
|
720
810
|
if (accountId) {
|
|
721
|
-
|
|
811
|
+
({ data } = await api.transactions.getTransactionsByAccount(bid, accountId, sinceDate, type, lastKnowledgeOfServer));
|
|
722
812
|
transactions = data.transactions;
|
|
723
813
|
} else if (categoryId) {
|
|
724
|
-
|
|
814
|
+
({ data } = await api.transactions.getTransactionsByCategory(bid, categoryId, sinceDate, type, lastKnowledgeOfServer));
|
|
725
815
|
transactions = data.transactions;
|
|
726
816
|
} else if (payeeId) {
|
|
727
|
-
|
|
817
|
+
({ data } = await api.transactions.getTransactionsByPayee(bid, payeeId, sinceDate, type, lastKnowledgeOfServer));
|
|
728
818
|
transactions = data.transactions;
|
|
729
819
|
} else if (month) {
|
|
730
|
-
|
|
820
|
+
({ data } = await api.transactions.getTransactionsByMonth(bid, month, sinceDate, type, lastKnowledgeOfServer));
|
|
731
821
|
transactions = data.transactions;
|
|
732
822
|
} else {
|
|
733
|
-
|
|
823
|
+
({ data } = await api.transactions.getTransactions(bid, sinceDate, type, lastKnowledgeOfServer));
|
|
734
824
|
transactions = data.transactions;
|
|
735
825
|
}
|
|
736
826
|
|
|
737
|
-
return ok(transactions.map(formatTransaction));
|
|
827
|
+
return ok(collection(data, "transactions", transactions.map(formatTransaction), lastKnowledgeOfServer));
|
|
738
828
|
})
|
|
739
829
|
);
|
|
740
830
|
|
|
@@ -753,7 +843,7 @@ server.registerTool(
|
|
|
753
843
|
|
|
754
844
|
server.registerTool(
|
|
755
845
|
"create_transaction",
|
|
756
|
-
{ description: "Create a new transaction. Amounts are in dollars (positive for inflows, negative for outflows). Note: future-dated transactions cannot be created here
|
|
846
|
+
{ description: "Create a new transaction. Amounts are in dollars (positive for inflows, negative for outflows). Note: future-dated transactions cannot be created here - use create_scheduled_transaction instead.", inputSchema: {
|
|
757
847
|
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
758
848
|
accountId: z.string().describe("Account ID"),
|
|
759
849
|
date: z.string().describe("Transaction date (YYYY-MM-DD)"),
|
|
@@ -785,7 +875,7 @@ server.registerTool(
|
|
|
785
875
|
|
|
786
876
|
server.registerTool(
|
|
787
877
|
"create_transactions",
|
|
788
|
-
{ description: "Create multiple transactions at once. Amounts are in dollars. Returns created transactions and any duplicate import IDs. Future-dated transactions are not supported
|
|
878
|
+
{ description: "Create multiple transactions at once. Amounts are in dollars. Returns created transactions and any duplicate import IDs. Future-dated transactions are not supported - use create_scheduled_transaction instead.", inputSchema: {
|
|
789
879
|
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
790
880
|
transactions: z.array(z.object({
|
|
791
881
|
accountId: z.string().describe("Account ID"),
|
|
@@ -906,45 +996,56 @@ server.registerTool(
|
|
|
906
996
|
// ==================== Scheduled Transactions ====================
|
|
907
997
|
|
|
908
998
|
function formatScheduledTransaction(t) {
|
|
909
|
-
|
|
999
|
+
const out = {
|
|
910
1000
|
id: t.id,
|
|
911
1001
|
date_first: t.date_first,
|
|
912
1002
|
date_next: t.date_next,
|
|
913
1003
|
frequency: t.frequency,
|
|
914
1004
|
amount: dollars(t.amount),
|
|
915
|
-
memo: t.memo,
|
|
916
|
-
flag_color: t.flag_color,
|
|
917
|
-
flag_name: t.flag_name,
|
|
1005
|
+
memo: t.memo ?? null,
|
|
1006
|
+
flag_color: t.flag_color ?? null,
|
|
1007
|
+
flag_name: t.flag_name ?? null,
|
|
918
1008
|
account_id: t.account_id,
|
|
919
1009
|
account_name: t.account_name,
|
|
920
|
-
payee_id: t.payee_id,
|
|
921
|
-
payee_name: t.payee_name,
|
|
922
|
-
category_id: t.category_id,
|
|
923
|
-
category_name: t.category_name,
|
|
924
|
-
transfer_account_id: t.transfer_account_id,
|
|
1010
|
+
payee_id: t.payee_id ?? null,
|
|
1011
|
+
payee_name: t.payee_name ?? null,
|
|
1012
|
+
category_id: t.category_id ?? null,
|
|
1013
|
+
category_name: t.category_name ?? null,
|
|
1014
|
+
transfer_account_id: t.transfer_account_id ?? null,
|
|
925
1015
|
deleted: t.deleted,
|
|
926
|
-
subtransactions: t.subtransactions?.map((s) =>
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1016
|
+
subtransactions: t.subtransactions?.map((s) =>
|
|
1017
|
+
withCurrencyFields(
|
|
1018
|
+
{
|
|
1019
|
+
id: s.id,
|
|
1020
|
+
scheduled_transaction_id: s.scheduled_transaction_id,
|
|
1021
|
+
amount: dollars(s.amount),
|
|
1022
|
+
memo: s.memo ?? null,
|
|
1023
|
+
payee_id: s.payee_id ?? null,
|
|
1024
|
+
payee_name: s.payee_name ?? null,
|
|
1025
|
+
category_id: s.category_id ?? null,
|
|
1026
|
+
category_name: s.category_name ?? null,
|
|
1027
|
+
transfer_account_id: s.transfer_account_id ?? null,
|
|
1028
|
+
deleted: s.deleted,
|
|
1029
|
+
},
|
|
1030
|
+
s,
|
|
1031
|
+
["amount"]
|
|
1032
|
+
)
|
|
1033
|
+
),
|
|
938
1034
|
};
|
|
1035
|
+
return withCurrencyFields(out, t, ["amount"]);
|
|
939
1036
|
}
|
|
940
1037
|
|
|
941
1038
|
server.registerTool(
|
|
942
1039
|
"list_scheduled_transactions",
|
|
943
|
-
{ description: "List all scheduled (recurring) transactions", inputSchema: {
|
|
944
|
-
|
|
1040
|
+
{ description: "List all scheduled (recurring) transactions", inputSchema: {
|
|
1041
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
1042
|
+
lastKnowledgeOfServer: z.number().int().nonnegative().optional().describe("Delta request server knowledge. When provided, returns { scheduled_transactions, server_knowledge }."),
|
|
1043
|
+
} },
|
|
1044
|
+
({ budgetId, lastKnowledgeOfServer }) =>
|
|
945
1045
|
run(async () => {
|
|
946
|
-
const { data } = await api.scheduledTransactions.getScheduledTransactions(resolveBudgetId(budgetId));
|
|
947
|
-
|
|
1046
|
+
const { data } = await api.scheduledTransactions.getScheduledTransactions(resolveBudgetId(budgetId), lastKnowledgeOfServer);
|
|
1047
|
+
const scheduledTransactions = data.scheduled_transactions.map(formatScheduledTransaction);
|
|
1048
|
+
return ok(collection(data, "scheduled_transactions", scheduledTransactions, lastKnowledgeOfServer));
|
|
948
1049
|
})
|
|
949
1050
|
);
|
|
950
1051
|
|
|
@@ -1012,7 +1113,7 @@ server.registerTool(
|
|
|
1012
1113
|
({ budgetId, scheduledTransactionId, accountId, date, frequency, amount, payeeId, payeeName, categoryId, memo, flagColor }) =>
|
|
1013
1114
|
run(async () => {
|
|
1014
1115
|
const bid = resolveBudgetId(budgetId);
|
|
1015
|
-
// PUT replaces the full resource
|
|
1116
|
+
// PUT replaces the full resource - fetch current values to merge with updates
|
|
1016
1117
|
const { data: current } = await api.scheduledTransactions.getScheduledTransactionById(bid, scheduledTransactionId);
|
|
1017
1118
|
const existing = current.scheduled_transaction;
|
|
1018
1119
|
|
|
@@ -1068,14 +1169,14 @@ server.registerTool(
|
|
|
1068
1169
|
for (const c of g.categories) {
|
|
1069
1170
|
if (c.hidden) continue;
|
|
1070
1171
|
if (c.name.toLowerCase().includes(q)) {
|
|
1071
|
-
matches.push({
|
|
1172
|
+
matches.push(withCurrencyFields({
|
|
1072
1173
|
id: c.id,
|
|
1073
1174
|
name: c.name,
|
|
1074
1175
|
group: g.name,
|
|
1075
1176
|
budgeted: dollars(c.budgeted),
|
|
1076
1177
|
activity: dollars(c.activity),
|
|
1077
1178
|
balance: dollars(c.balance),
|
|
1078
|
-
});
|
|
1179
|
+
}, c, ["budgeted", "activity", "balance"]));
|
|
1079
1180
|
}
|
|
1080
1181
|
}
|
|
1081
1182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oliverames/ynab-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "YNAB MCP server with full API coverage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"test": "node test.js"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
19
|
-
"ynab": "^2.5.0"
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
19
|
+
"ynab": "^2.5.0",
|
|
20
|
+
"zod": "^4.3.6"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
20
24
|
}
|
|
21
25
|
}
|