actual-mcp-server 0.5.7 → 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 +54 -2
- package/dist/package.json +3 -2
- package/dist/src/index.js +16 -0
- package/dist/src/tools/transactions_uncategorized.js +73 -28
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ Most Actual Budget MCP implementations are simple stdio bridges designed for sin
|
|
|
47
47
|
## Table of Contents
|
|
48
48
|
|
|
49
49
|
- [Quick Start](#quick-start)
|
|
50
|
+
- [Upgrading](#upgrading)
|
|
50
51
|
- [Available Tools](#available-tools)
|
|
51
52
|
- [Configuration](#configuration)
|
|
52
53
|
- [Multi-Budget Switching](#multi-budget-switching)
|
|
@@ -234,6 +235,51 @@ See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all
|
|
|
234
235
|
|
|
235
236
|
---
|
|
236
237
|
|
|
238
|
+
## Upgrading
|
|
239
|
+
|
|
240
|
+
### Docker (Option A)
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
docker pull ghcr.io/agigante80/actual-mcp-server:latest
|
|
244
|
+
docker stop actual-mcp-server-backend
|
|
245
|
+
docker rm actual-mcp-server-backend
|
|
246
|
+
# Re-run the original docker run command with the same flags and volumes
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Also available on Docker Hub: `docker pull agigante80/actual-mcp-server:latest`
|
|
250
|
+
|
|
251
|
+
### Docker Compose (Option B)
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
docker compose pull
|
|
255
|
+
docker compose --profile production up -d
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### npm / cloned repo (Option C)
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
git pull
|
|
262
|
+
npm install
|
|
263
|
+
npm run build
|
|
264
|
+
# Then restart the server
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### npx / stdio (Options C & D)
|
|
268
|
+
|
|
269
|
+
If you run `npx actual-mcp-server` without a globally installed version, npx fetches the latest from the registry automatically. But if you previously installed it globally (`npm install -g actual-mcp-server`), the global install takes precedence — you must upgrade it explicitly:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Upgrade the global install
|
|
273
|
+
npm install -g actual-mcp-server
|
|
274
|
+
|
|
275
|
+
# Or force the registry version without touching your global install
|
|
276
|
+
npx actual-mcp-server@latest --http
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
For Claude Desktop (stdio), restart Claude after upgrading.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
237
283
|
## Available Tools
|
|
238
284
|
|
|
239
285
|
**63 tools** across 12 categories. All tools use the `actual_<category>_<action>` naming convention.
|
|
@@ -250,7 +296,7 @@ See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all
|
|
|
250
296
|
| `actual_accounts_reopen` | Reopen closed account |
|
|
251
297
|
| `actual_accounts_get_balance` | Get account balance at a date |
|
|
252
298
|
|
|
253
|
-
### Transactions (
|
|
299
|
+
### Transactions (13)
|
|
254
300
|
|
|
255
301
|
**Standard (6)**
|
|
256
302
|
|
|
@@ -263,6 +309,12 @@ See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all
|
|
|
263
309
|
| `actual_transactions_update` | Update a transaction |
|
|
264
310
|
| `actual_transactions_delete` | Delete a transaction |
|
|
265
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
|
+
|
|
266
318
|
**Exclusive ActualQL-powered (6)** — unique to this MCP server
|
|
267
319
|
|
|
268
320
|
| Tool | Description |
|
|
@@ -678,4 +730,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
678
730
|
|
|
679
731
|
---
|
|
680
732
|
|
|
681
|
-
**Version:** 0.
|
|
733
|
+
**Version:** 0.6.0 | **Tool Count:** 63 (verified LibreChat-compatible)
|
package/dist/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
8
8
|
},
|
|
9
|
-
"description": "MCP server with
|
|
9
|
+
"description": "MCP server with 63 tools for AI-driven financial management with Actual Budget — HTTP and stdio transports, LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
|
|
10
|
+
"homepage": "https://github.com/agigante80/actual-mcp-server#readme",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
13
|
"url": "https://github.com/agigante80/actual-mcp-server"
|
package/dist/src/index.js
CHANGED
|
@@ -89,6 +89,22 @@ if (unknownFlags.length > 0) {
|
|
|
89
89
|
console.error(`Unknown option: ${unknownFlags.join(', ')}\nRun \`actual-mcp-server --help\` for usage.`);
|
|
90
90
|
process.exit(1);
|
|
91
91
|
}
|
|
92
|
+
// Early transport mode check — before dotenv and dynamic imports.
|
|
93
|
+
// Without this, the config validation error fires first and confuses users who
|
|
94
|
+
// simply forgot to pass --http or --stdio.
|
|
95
|
+
if (!argsEarly.includes('--http') &&
|
|
96
|
+
!argsEarly.includes('--stdio') &&
|
|
97
|
+
!argsEarly.includes('--test-actual-connection') &&
|
|
98
|
+
!argsEarly.includes('--test-mcp-client') &&
|
|
99
|
+
!argsEarly.includes('--help') && !argsEarly.includes('-h') &&
|
|
100
|
+
!argsEarly.includes('--version') && !argsEarly.includes('-v')) {
|
|
101
|
+
console.error('No transport mode specified.\n\n' +
|
|
102
|
+
'Usage:\n' +
|
|
103
|
+
' actual-mcp-server --http # HTTP server (LibreChat / LobeChat / multi-user)\n' +
|
|
104
|
+
' actual-mcp-server --stdio # stdio transport (Claude Desktop / Claude Code)\n\n' +
|
|
105
|
+
'Run actual-mcp-server --help for all options.\n');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
92
108
|
if (argsEarly.includes('--help') || argsEarly.includes('-h') ||
|
|
93
109
|
argsEarly.includes('--version') || argsEarly.includes('-v')) {
|
|
94
110
|
const pkg = await import('../package.json', { with: { type: 'json' } });
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* actual_transactions_uncategorized
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
21
|
-
|
|
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:
|
|
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
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
accountList.map((acc) => [acc.id, acc.name]));
|
|
50
71
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
-
(txn) => txn
|
|
52
|
-
|
|
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 =
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
8
8
|
},
|
|
9
|
-
"description": "MCP server with
|
|
9
|
+
"description": "MCP server with 63 tools for AI-driven financial management with Actual Budget — HTTP and stdio transports, LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
|
|
10
|
+
"homepage": "https://github.com/agigante80/actual-mcp-server#readme",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
13
|
"url": "https://github.com/agigante80/actual-mcp-server"
|