@oliverames/ynab-mcp-server 1.6.0 → 1.7.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.
Files changed (2) hide show
  1. package/index.js +42 -11
  2. package/package.json +2 -1
package/index.js CHANGED
@@ -793,7 +793,7 @@ function formatTransaction(t) {
793
793
 
794
794
  server.registerTool(
795
795
  "get_transactions",
796
- { description: "Get transactions with optional filters. Use type='unapproved' or type='uncategorized' to filter. Optionally filter by account, category, payee, or month.", inputSchema: {
796
+ { description: "Get transactions with optional filters. Use type='unapproved' or type='uncategorized' to filter. Optionally filter by account, category, payee, or month. Each returned transaction includes 'import_payee_name_original' — the raw merchant string from the bank import (e.g. 'AplPay LS ONION RIVEMONTPELIER VT') — which encodes processor flag, merchant name (often longer than the cleaned payee_name), and city+state. This is the primary disambiguation field when payee_name is truncated or ambiguous. Note: large date ranges (6+ months on a busy budget) can return 50KB+ of data; narrow with categoryId/payeeId/month filters when possible.", inputSchema: {
797
797
  budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
798
798
  sinceDate: z.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD)"),
799
799
  type: z.enum(["unapproved", "uncategorized"]).optional().describe("Filter by approval/categorization status"),
@@ -956,7 +956,7 @@ server.registerTool(
956
956
 
957
957
  server.registerTool(
958
958
  "update_transactions",
959
- { description: "Batch update multiple transactions. Each transaction object must include its id and the fields to update.", inputSchema: {
959
+ { description: "Batch update multiple transactions. Each transaction object must include its id and the fields to update. IMPORTANT: only use transaction IDs extracted from get_transactions / review_unapproved results — never compose IDs by hand (fabricated IDs return 'transaction does not exist in this budget' errors). For combined category+approval changes in one round trip, include both 'categoryId' and 'approved: true' in the same entry. The response returns the full updated transaction objects, which can exceed 50KB for batches over ~50 transactions; if the response overflows, the update still succeeded — verify by counting 'approved: true' occurrences in the saved result file.", inputSchema: {
960
960
  budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
961
961
  transactions: z
962
962
  .array(
@@ -1211,8 +1211,11 @@ server.registerTool(
1211
1211
 
1212
1212
  server.registerTool(
1213
1213
  "review_unapproved",
1214
- { description: "Get all unapproved transactions grouped by status: those already categorized (ready to approve) and those still uncategorized (need category first). Each transaction includes a 'flags' array: manually_entered (not bank-imported), match_broken (matched reference may be stale), scheduled_transaction_realized, new_payee (no transaction history for this payee), no_prior_amount_match (novel amount for this payee), category_drift:was_X (payee categorized differently before). Never approve uncategorized transactions without explicit user instruction.", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
1215
- ({ budgetId }) =>
1214
+ { description: "Get all unapproved transactions grouped by status: those already categorized (ready to approve) and those still uncategorized (need category first). Each transaction includes a 'flags' array: manually_entered (not bank-imported), match_broken (matched reference is stale — CANNOT be fixed via this API, requires YNAB web/iOS UI), scheduled_transaction_realized, new_payee (no transaction history for this payee), no_prior_amount_match (novel amount for this payee), category_drift:was_X (payee categorized differently before). Never approve uncategorized transactions without explicit user instruction. For large budgets the full response can exceed 100KB; pass summary:true to get counts + by-payee aggregates without per-transaction detail.", inputSchema: {
1215
+ budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
1216
+ summary: z.boolean().optional().describe("If true, omit per-transaction details from the response and return only counts + by-payee aggregates (for both ready_to_approve and needs_category_first). Use this when the full unapproved queue is large; drill into specifics with get_transactions afterwards."),
1217
+ } },
1218
+ ({ budgetId, summary }) =>
1216
1219
  run(async () => {
1217
1220
  const bid = resolveBudgetId(budgetId);
1218
1221
 
@@ -1283,25 +1286,53 @@ server.registerTool(
1283
1286
  const groups = Object.values(byPayee).map((g) => {
1284
1287
  // Aggregate flags across all transactions in the group (deduplicated)
1285
1288
  const allFlags = [...new Set(g.transactions.flatMap((t) => t.flags))];
1286
- return {
1287
- ...g,
1289
+ const base = {
1290
+ payee: g.payee,
1291
+ category_name: g.category_name,
1288
1292
  count: g.transactions.length,
1289
1293
  total: g.transactions.reduce((sum, t) => sum + t.amount, 0),
1290
1294
  flags: allFlags,
1291
1295
  };
1296
+ return summary ? base : { ...base, transactions: g.transactions };
1292
1297
  });
1293
1298
 
1299
+ // Build uncategorized payload — full transactions by default, by-payee aggregates when summary:true
1300
+ const uncategorizedPayload = (() => {
1301
+ if (!summary) return uncategorized;
1302
+ const byPayeeUncat = {};
1303
+ for (const t of uncategorized) {
1304
+ const key = t.payee_name || "Unknown Payee";
1305
+ if (!byPayeeUncat[key]) byPayeeUncat[key] = { payee_name: key, count: 0, total: 0, flags: new Set() };
1306
+ byPayeeUncat[key].count += 1;
1307
+ byPayeeUncat[key].total += t.amount;
1308
+ for (const f of t.flags) byPayeeUncat[key].flags.add(f);
1309
+ }
1310
+ return Object.values(byPayeeUncat).map((g) => ({
1311
+ payee_name: g.payee_name,
1312
+ count: g.count,
1313
+ total: g.total,
1314
+ flags: [...g.flags],
1315
+ }));
1316
+ })();
1317
+
1318
+ const needsCategoryFirst = {
1319
+ count: uncategorized.length,
1320
+ warning: "Do NOT approve these without assigning a category first",
1321
+ };
1322
+ if (summary) {
1323
+ needsCategoryFirst.payees = uncategorizedPayload;
1324
+ } else {
1325
+ needsCategoryFirst.transactions = uncategorizedPayload;
1326
+ }
1327
+
1294
1328
  return ok({
1295
1329
  total: flaggedTxns.length,
1330
+ summary: !!summary,
1296
1331
  ready_to_approve: {
1297
1332
  count: categorized.length,
1298
1333
  by_payee: groups,
1299
1334
  },
1300
- needs_category_first: {
1301
- count: uncategorized.length,
1302
- warning: "Do NOT approve these without assigning a category first",
1303
- transactions: uncategorized,
1304
- },
1335
+ needs_category_first: needsCategoryFirst,
1305
1336
  });
1306
1337
  })
1307
1338
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oliverames/ynab-mcp-server",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "YNAB MCP server with full API coverage",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "start": "node index.js",
15
+ "pretest": "[ -d node_modules ] || npm ci --silent --no-audit --no-fund",
15
16
  "test": "node test.js"
16
17
  },
17
18
  "dependencies": {