actual-mcp-server 0.5.8 → 0.6.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 CHANGED
@@ -296,7 +296,7 @@ For Claude Desktop (stdio), restart Claude after upgrading.
296
296
  | `actual_accounts_reopen` | Reopen closed account |
297
297
  | `actual_accounts_get_balance` | Get account balance at a date |
298
298
 
299
- ### Transactions (12)
299
+ ### Transactions (13)
300
300
 
301
301
  **Standard (6)**
302
302
 
@@ -309,6 +309,12 @@ For Claude Desktop (stdio), restart Claude after upgrading.
309
309
  | `actual_transactions_update` | Update a transaction |
310
310
  | `actual_transactions_delete` | Delete a transaction |
311
311
 
312
+ **Utility (1)**
313
+
314
+ | Tool | Description |
315
+ |------|-------------|
316
+ | `actual_transactions_uncategorized` | Summary of uncategorized transactions (totalCount, totalAmount, per-account breakdown); pass `includeTransactions:true` for paginated rows |
317
+
312
318
  **Exclusive ActualQL-powered (6)** — unique to this MCP server
313
319
 
314
320
  | Tool | Description |
@@ -724,4 +730,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
724
730
 
725
731
  ---
726
732
 
727
- **Version:** 0.5.8 | **Tool Count:** 63 (verified LibreChat-compatible)
733
+ **Version:** 0.6.0 | **Tool Count:** 63 (verified LibreChat-compatible)
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.5.8",
4
+ "version": "0.6.0",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -1,15 +1,13 @@
1
1
  /**
2
2
  * actual_transactions_uncategorized
3
3
  *
4
- * List transactions that have no category assigned.
4
+ * Returns a summary of uncategorized transactions by default (totalCount, totalAmount,
5
+ * per-account breakdown). Pass includeTransactions:true to also receive a paginated
6
+ * transaction list with limit/offset/hasMore.
5
7
  *
6
8
  * Concept and implementation adapted from the ZanzyTHEbar fork:
7
9
  * https://github.com/ZanzyTHEbar/actual-mcp-server/blob/main/src/tools/transactions_uncategorized.ts
8
10
  * Credit: ZanzyTHEbar (https://github.com/ZanzyTHEbar)
9
- *
10
- * Adapted for this project's conventions:
11
- * - No wrapToolCall — uses direct call() pattern
12
- * - Returns { transactions, count, summary, dateRange } matching the search_by_* shape
13
11
  */
14
12
  import { z } from 'zod';
15
13
  import adapter from '../lib/actual-adapter.js';
@@ -17,12 +15,30 @@ import { CommonSchemas } from '../lib/schemas/common.js';
17
15
  const InputSchema = z.object({
18
16
  startDate: CommonSchemas.date.optional().describe('Start date in YYYY-MM-DD format (default: first day of current month)'),
19
17
  endDate: CommonSchemas.date.optional().describe('End date in YYYY-MM-DD format (default: today)'),
20
- accountId: CommonSchemas.accountId.optional().describe('Filter by specific account ID (optional)'),
21
- limit: z.number().optional().default(500).describe('Maximum number of transactions to return (default: 500)'),
18
+ accountId: CommonSchemas.accountId.optional().describe('Filter to a specific account ID (optional)'),
19
+ includeTransactions: z.boolean().optional().default(false).describe('When true, include paginated transaction rows in the response (default: false)'),
20
+ limit: z.number().int().min(1).max(1000).optional().default(50).describe('Max transactions per page when includeTransactions is true (default: 50, max: 1000)'),
21
+ offset: z.number().int().min(0).optional().default(0).describe('Pagination start index when includeTransactions is true (default: 0)'),
22
22
  });
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ function isUncategorized(txn, excludedAccountIds) {
25
+ return (txn?.category == null &&
26
+ txn?.transfer_id == null &&
27
+ txn?.is_parent !== true &&
28
+ txn?.starting_balance_flag !== true &&
29
+ !excludedAccountIds.has(txn?.account));
30
+ }
23
31
  const tool = {
24
32
  name: 'actual_transactions_uncategorized',
25
- description: 'List uncategorized transactions (category is null/unset). Excludes transfers, split-transaction parents, opening balance entries, off-budget accounts, and closed accounts — matching Actual Budget\'s own Uncategorized view. Defaults to the current month unless a date range is provided. Returns { transactions, count, summary: { totalAmount }, dateRange }.',
33
+ description: [
34
+ 'Get uncategorized transactions summary and optional paginated list.',
35
+ 'Default response: { totalCount, totalAmount, byAccount: [{accountId, accountName, count, totalAmount}], dateRange }.',
36
+ 'Pass includeTransactions:true to also receive transactions[], count, hasMore, offset, limit.',
37
+ 'Use limit (default 50, max 1000) and offset for pagination.',
38
+ 'Excludes: transfers, split-transaction parents, opening balance entries, off-budget accounts, and closed accounts.',
39
+ 'When accountId is provided, all fields are scoped to that account.',
40
+ 'Note: the legacy summary.totalAmount field has been removed — totalAmount is now at the top level.',
41
+ ].join(' '),
26
42
  inputSchema: InputSchema,
27
43
  call: async (args, _meta) => {
28
44
  const input = InputSchema.parse(args || {});
@@ -31,40 +47,69 @@ const tool = {
31
47
  const startDate = input.startDate || firstDayOfMonth.toISOString().split('T')[0];
32
48
  const endDate = input.endDate || today.toISOString().split('T')[0];
33
49
  const accountId = input.accountId ?? undefined;
34
- // Fetch transactions first. When accountId is undefined, rawGetTransactions does
35
- // a full table scan (no account filter) this reliably returns newly written
36
- // transactions even across separate WAL sessions in CI Docker. Filtering by
37
- // accountId uses a SQLite index that can lag across sessions.
50
+ // Full table scan when no accountId reliably returns newly written transactions
51
+ // across separate WAL sessions in CI Docker. Filtering by accountId uses an index
52
+ // that can lag across sessions.
38
53
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
54
  const txnRaw = await adapter.getTransactions(accountId, startDate, endDate);
40
55
  const txns = Array.isArray(txnRaw) ? txnRaw : [];
41
- // Fetch accounts to build exclusion set: off-budget + closed accounts.
56
+ // Fetch accounts for exclusion set (off-budget + closed) and name lookup.
57
+ // Two separate withActualApi sessions is intentional for a read-only tool —
58
+ // keeps each call isolated. If latency becomes a concern, both could be merged
59
+ // into a single withActualApi using the raw API directly.
42
60
  const accounts = await adapter.getAccounts();
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ const accountList = Array.isArray(accounts) ? accounts : [];
43
63
  const excludedAccountIds = new Set(
44
64
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
- (Array.isArray(accounts) ? accounts : [])
65
+ accountList
46
66
  .filter((acc) => acc?.offbudget === true || acc?.closed === true)
47
67
  .map((acc) => acc.id));
68
+ const accountNameMap = new Map(
48
69
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
- const uncategorized = txns.filter(
70
+ accountList.map((acc) => [acc.id, acc.name]));
50
71
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
- (txn) => txn?.category == null &&
52
- txn?.transfer_id == null &&
53
- txn?.is_parent !== true &&
54
- txn?.starting_balance_flag !== true &&
55
- !excludedAccountIds.has(txn?.account));
56
- const limited = uncategorized.slice(0, input.limit ?? 500);
72
+ const uncategorized = txns.filter((txn) => isUncategorized(txn, excludedAccountIds));
73
+ const totalCount = uncategorized.length;
57
74
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
- const totalAmount = limited.reduce((sum, txn) => {
59
- const amount = typeof txn?.amount === 'number' ? txn.amount : 0;
60
- return sum + amount;
75
+ const totalAmount = uncategorized.reduce((sum, txn) => {
76
+ return sum + (typeof txn?.amount === 'number' ? txn.amount : 0);
61
77
  }, 0);
62
- return {
63
- transactions: limited,
64
- count: limited.length,
65
- summary: { totalAmount },
78
+ // Per-account breakdown — one entry per on-budget open account with ≥1 uncategorized txn
79
+ const byAccountMap = new Map();
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ for (const txn of uncategorized) {
82
+ const acctId = txn?.account;
83
+ if (!acctId)
84
+ continue;
85
+ const entry = byAccountMap.get(acctId) ?? { count: 0, totalAmount: 0 };
86
+ entry.count += 1;
87
+ entry.totalAmount += typeof txn?.amount === 'number' ? txn.amount : 0;
88
+ byAccountMap.set(acctId, entry);
89
+ }
90
+ const byAccount = Array.from(byAccountMap.entries()).map(([acctId, data]) => ({
91
+ accountId: acctId,
92
+ accountName: accountNameMap.get(acctId) ?? acctId,
93
+ count: data.count,
94
+ totalAmount: data.totalAmount,
95
+ }));
96
+ const result = {
97
+ totalCount,
98
+ totalAmount,
99
+ byAccount,
66
100
  dateRange: { startDate, endDate },
67
101
  };
102
+ if (input.includeTransactions) {
103
+ const limit = input.limit ?? 50;
104
+ const offset = input.offset ?? 0;
105
+ const page = uncategorized.slice(offset, offset + limit);
106
+ result.transactions = page;
107
+ result.count = page.length;
108
+ result.hasMore = offset + page.length < totalCount;
109
+ result.offset = offset;
110
+ result.limit = limit;
111
+ }
112
+ return result;
68
113
  },
69
114
  };
70
115
  export default tool;
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.5.8",
4
+ "version": "0.6.0",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"