actual-mcp-server 0.19.2 → 0.19.3

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
@@ -778,7 +778,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
778
778
 
779
779
  ---
780
780
 
781
- **Version:** 0.19.2 | **Tool Count:** 77 (verified LibreChat-compatible)
781
+ **Version:** 0.19.3 | **Tool Count:** 77 (verified LibreChat-compatible)
782
782
 
783
783
  ## Sponsor
784
784
 
@@ -258,6 +258,13 @@ export function summarizeAccountFlow(snapshot, input) {
258
258
  const counterpart = transaction.transfer_id ? maps.transactions.get(transaction.transfer_id) : undefined;
259
259
  const payee = transaction.payee ? maps.payees.get(transaction.payee) : undefined;
260
260
  const counterpartAccountId = counterpart?.account ?? payee?.transfer_acct ?? null;
261
+ // #428: when the counterpart cannot be resolved (no counterpart row and no payee transfer_acct),
262
+ // counterpartSelected is false and the leg is bucketed by SIGN into intoSelection / outOfSelection.
263
+ // This is the deterministic rule: within-selection membership requires POSITIVE proof both legs are
264
+ // selected, which an unresolvable counterpart cannot give. The outcome is not silent: the
265
+ // transfersByAccount row below carries matchedCounterpart=false, so a caller can see the leg was
266
+ // classified without a resolved counterparty. netTransferEffect (line above) already counted the
267
+ // amount, so reconciliation stays exact regardless of the bucket.
261
268
  const counterpartSelected = counterpartAccountId ? selected.has(counterpartAccountId) : false;
262
269
  if (counterpartSelected) {
263
270
  withinSelectionTimingEffect += amount;
@@ -290,14 +297,25 @@ export function summarizeAccountFlow(snapshot, input) {
290
297
  });
291
298
  }
292
299
  }
293
- // The tool handler always supplies balances (via balanceAccountIds), so difference is exact here.
294
- // If a future caller omits them, both reduce to 0 and difference reads as a false "exact": #428.
295
- const openingBalance = uniqueAccountIds.reduce((sum, id) => sum + integer(snapshot.openingBalances?.[id] ?? 0, `Opening balance for ${id}`), 0);
296
- const closingBalance = uniqueAccountIds.reduce((sum, id) => sum + integer(snapshot.closingBalances?.[id] ?? 0, `Closing balance for ${id}`), 0);
297
- const balanceChange = closingBalance - openingBalance;
300
+ // #428: balances are OPTIONAL on the snapshot. The tool handler always supplies them (via
301
+ // balanceAccountIds), so on the tool path this is true and reconciliation is exact by construction.
302
+ // A caller that omits either map, though, would otherwise reduce both to 0 and read difference as a
303
+ // false "exact" zero. The check is on the MAP being undefined (entirely unpopulated), NOT a per-id
304
+ // lookup: the `?? 0` per-id default is legitimate for a genuine zero-balance account.
305
+ const balancesAvailable = snapshot.openingBalances !== undefined && snapshot.closingBalances !== undefined;
306
+ const openingBalance = balancesAvailable
307
+ ? uniqueAccountIds.reduce((sum, id) => sum + integer(snapshot.openingBalances?.[id] ?? 0, `Opening balance for ${id}`), 0)
308
+ : null;
309
+ const closingBalance = balancesAvailable
310
+ ? uniqueAccountIds.reduce((sum, id) => sum + integer(snapshot.closingBalances?.[id] ?? 0, `Closing balance for ${id}`), 0)
311
+ : null;
312
+ const balanceChange = openingBalance !== null && closingBalance !== null ? closingBalance - openingBalance : null;
298
313
  const netExternalCashFlow = income - incomeReversals + credits + uncategorizedInflows - expenseOutflow;
299
314
  const totalAdjustments = startingBalance;
300
315
  const calculatedBalanceChange = netExternalCashFlow + netTransferEffect + totalAdjustments;
316
+ // When balances are unavailable, difference must NOT read as a reconciled value. Null it so a
317
+ // caller cannot mistake an unpopulated run for a reconciled one.
318
+ const difference = balanceChange !== null ? balanceChange - calculatedBalanceChange : null;
301
319
  return {
302
320
  dateRange: { startDate: input.startDate, endDate: input.endDate },
303
321
  accounts: uniqueAccountIds.map(id => ({ id, name: maps.accounts.get(id)?.name ?? id })),
@@ -322,9 +340,12 @@ export function summarizeAccountFlow(snapshot, input) {
322
340
  transfersByAccount: transfersByAccount.sort((a, b) => `${a.date}:${a.fromAccountId}:${a.toAccountId}`.localeCompare(`${b.date}:${b.fromAccountId}:${b.toAccountId}`)),
323
341
  adjustments: { startingBalance, total: totalAdjustments },
324
342
  reconciliation: {
343
+ // #428: balancesAvailable is false when the snapshot omitted the balance maps; difference and
344
+ // actualBalanceChange are then null so an unpopulated run cannot read as a reconciled one.
345
+ balancesAvailable,
325
346
  calculatedBalanceChange,
326
347
  actualBalanceChange: balanceChange,
327
- difference: balanceChange - calculatedBalanceChange,
348
+ difference,
328
349
  },
329
350
  };
330
351
  }
@@ -24,7 +24,7 @@ export default createTool({
24
24
  'Explain the exact balance change across a selected set of accounts over an inclusive date range, in integer cents.',
25
25
  'Separates external income, expense outflow, credits, uncategorized inflows, starting-balance adjustments, and transfers into, out of, or within the selection.',
26
26
  'Transfers are detected from transfer_id and are never spending or income.',
27
- 'openingBalance is immediately before startDate and closingBalance is at the end of the inclusive endDate; reconciliation.difference is exact and transfersByAccount names the funding counterparties.',
27
+ 'openingBalance is immediately before startDate and closingBalance is at the end of the inclusive endDate; reconciliation.difference is exact (reconciliation.balancesAvailable is true) and transfersByAccount names the funding counterparties.',
28
28
  'Amounts are in cents: $100.00 = 10000. Dates are YYYY-MM-DD.',
29
29
  ].join(' '),
30
30
  schema: InputSchema,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.19.2",
4
+ "version": "0.19.3",
5
5
  "engines": {
6
6
  "node": ">=22.0.0",
7
7
  "npm": ">=10.0.0"