actual-mcp-server 0.5.4 → 0.5.6
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 +19 -9
- package/dist/package.json +7 -1
- package/dist/src/actualToolsManager.js +1 -0
- package/dist/src/config.js +5 -1
- package/dist/src/index.js +9 -0
- package/dist/src/lib/actual-adapter.js +42 -2
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/transfers_create.js +31 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Actual MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/actual-mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/actual-mcp-server)
|
|
3
5
|
[](https://opensource.org/licenses/MIT)
|
|
4
6
|
[](https://nodejs.org/)
|
|
5
7
|
[](https://www.typescriptlang.org/)
|
|
@@ -17,13 +19,13 @@ Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/
|
|
|
17
19
|
┌─────────────┐ MCP/HTTP ┌──────────────────┐ Actual API ┌──────────────┐
|
|
18
20
|
│ LibreChat │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
|
|
19
21
|
│ LobeChat │ │ Server │ │ Budget │
|
|
20
|
-
│ (remote) │ │ (
|
|
22
|
+
│ (remote) │ │ (63 tools) │ │ Server │
|
|
21
23
|
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
22
24
|
|
|
23
25
|
┌─────────────┐ MCP/stdio ┌──────────────────┐ Actual API ┌──────────────┐
|
|
24
26
|
│ Claude │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
|
|
25
27
|
│ Desktop │ │ Server │ │ Budget │
|
|
26
|
-
│ (local) │ │ (
|
|
28
|
+
│ (local) │ │ (63 tools) │ │ Server │
|
|
27
29
|
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
28
30
|
```
|
|
29
31
|
|
|
@@ -38,7 +40,7 @@ Most Actual Budget MCP implementations are simple stdio bridges designed for sin
|
|
|
38
40
|
- **Multi-user ready with OIDC.** Secure every session with JWKS-validated JWTs and per-user budget ACLs — no shared tokens required.
|
|
39
41
|
- **Production-grade reliability.** Connection pooling (up to 15 concurrent sessions), automatic retry with exponential backoff, and a full test suite (unit + E2E + integration).
|
|
40
42
|
|
|
41
|
-
> **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All
|
|
43
|
+
> **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All 63 tools tested end-to-end. Any MCP-compatible client should work.
|
|
42
44
|
|
|
43
45
|
---
|
|
44
46
|
|
|
@@ -169,7 +171,7 @@ Add to `claude_desktop_config.json` (see [docs/guides/MCP_CLIENTS_SETUP.md](docs
|
|
|
169
171
|
}
|
|
170
172
|
```
|
|
171
173
|
|
|
172
|
-
> **No token needed.** stdio runs as a local process owned by your user — the transport itself is the security boundary. All
|
|
174
|
+
> **No token needed.** stdio runs as a local process owned by your user — the transport itself is the security boundary. All 63 tools are available.
|
|
173
175
|
>
|
|
174
176
|
> **`MCP_BRIDGE_DATA_DIR` should be an absolute path** — without it, the data directory resolves relative to wherever the client spawns the process, which can be unpredictable. The directory is created automatically on first run.
|
|
175
177
|
|
|
@@ -234,7 +236,7 @@ See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all
|
|
|
234
236
|
|
|
235
237
|
## Available Tools
|
|
236
238
|
|
|
237
|
-
**
|
|
239
|
+
**63 tools** across 12 categories. All tools use the `actual_<category>_<action>` naming convention.
|
|
238
240
|
|
|
239
241
|
### Accounts (7)
|
|
240
242
|
|
|
@@ -272,6 +274,14 @@ See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all
|
|
|
272
274
|
| `actual_transactions_summary_by_category` | Spending summary grouped by category |
|
|
273
275
|
| `actual_transactions_summary_by_payee` | Top vendors with totals and counts |
|
|
274
276
|
|
|
277
|
+
### Transfers (1)
|
|
278
|
+
|
|
279
|
+
| Tool | Description |
|
|
280
|
+
|------|-------------|
|
|
281
|
+
| `actual_transfers_create` | Create a paired transfer between two accounts (debit + credit linked by `transfer_id`, identical to UI "Make Transfer") |
|
|
282
|
+
|
|
283
|
+
> **How transfers work under the hood** — Actual Budget requires the `runTransfers: true` option when adding transactions so that both sides (the debit on the source account and the credit on the destination account) are created and linked via a shared `transfer_id`. Prior to v0.5.6, the adapter forwarded a hardcoded empty options object `{}` to `rawAddTransactions`, silently dropping any options including this flag. This meant that calling `actual_transfers_create` would appear to succeed but only one side of the transfer would be recorded. The fix ensures options are forwarded correctly; use `actual_transfers_create` (not `actual_transactions_create`) for all account-to-account moves.
|
|
284
|
+
|
|
275
285
|
### Categories (4)
|
|
276
286
|
|
|
277
287
|
`actual_categories_get` · `actual_categories_create` · `actual_categories_update` · `actual_categories_delete`
|
|
@@ -446,7 +456,7 @@ stdio is the simplest way to connect Claude Desktop directly to Actual Budget. T
|
|
|
446
456
|
- No auth token — process ownership is the security boundary
|
|
447
457
|
- All logs go to stderr so they never corrupt the JSON-RPC framing on stdout
|
|
448
458
|
- The process exits when stdin closes (Claude Desktop shutting down)
|
|
449
|
-
- All
|
|
459
|
+
- All 63 tools are available, identical to HTTP mode
|
|
450
460
|
|
|
451
461
|
**Start manually to verify:**
|
|
452
462
|
|
|
@@ -523,7 +533,7 @@ See [AI Client Setup — OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authenticatio
|
|
|
523
533
|
| Command | What It Tests | Requires Live Server |
|
|
524
534
|
|---------|---------------|---------------------|
|
|
525
535
|
| `npm run build` | TypeScript compilation | No |
|
|
526
|
-
| `npm run test:unit-js` |
|
|
536
|
+
| `npm run test:unit-js` | 63-tool smoke, schema validation, auth ACL | No |
|
|
527
537
|
| `npm run test:adapter` | Adapter, retry logic, concurrency | No |
|
|
528
538
|
| `npm run test:e2e` | MCP protocol compliance (Playwright) | No |
|
|
529
539
|
| `npm run test:e2e:docker:full` | Full stack integration | Yes (Docker) |
|
|
@@ -565,7 +575,7 @@ Several MCP servers exist for personal finance management. Here's how this proje
|
|
|
565
575
|
| **Version** | v0.4.26 | v1.11.1 | v0.2.0 | v0.1.0 |
|
|
566
576
|
| **Budget App** | Actual Budget (self-hosted) | Actual Budget (self-hosted) | Actual Budget (self-hosted) | YNAB (cloud, subscription) |
|
|
567
577
|
| **Language** | TypeScript / Node.js | TypeScript / Node.js | TypeScript / Node.js | Python |
|
|
568
|
-
| **Tool Count** | **
|
|
578
|
+
| **Tool Count** | **63** | ~22 | 18 | 9 |
|
|
569
579
|
| **— Setup & Distribution —** |||||
|
|
570
580
|
| **Transport** | HTTP + stdio | STDIO + SSE option | STDIO | STDIO |
|
|
571
581
|
| **Docker support** | ✅ Full (image + Compose) | ✅ Image only | ❌ | ❌ |
|
|
@@ -668,4 +678,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
668
678
|
|
|
669
679
|
---
|
|
670
680
|
|
|
671
|
-
**Version:** 0.5.
|
|
681
|
+
**Version:** 0.5.6 | **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.
|
|
4
|
+
"version": "0.5.6",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -58,13 +58,18 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@actual-app/api": "^26.4.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
|
+
"debug": "^4.4.3",
|
|
61
62
|
"dotenv": "^17.4.1",
|
|
62
63
|
"express": "^5.2.1",
|
|
64
|
+
"jose": "^6.1.3",
|
|
63
65
|
"mcp-auth": "^0.2.0",
|
|
64
66
|
"winston": "^3.18.3",
|
|
65
67
|
"winston-daily-rotate-file": "^5.0.0",
|
|
66
68
|
"zod": "^4.0.0"
|
|
67
69
|
},
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"prom-client": "*"
|
|
72
|
+
},
|
|
68
73
|
"overrides": {
|
|
69
74
|
"ajv": "8.18.0",
|
|
70
75
|
"qs": "6.14.2"
|
|
@@ -76,6 +81,7 @@
|
|
|
76
81
|
"@playwright/test": "^1.59.1",
|
|
77
82
|
"@types/express": "^5.0.3",
|
|
78
83
|
"@types/node": "^25.6.0",
|
|
84
|
+
"node-fetch": "^3.3.2",
|
|
79
85
|
"tsconfig-paths": "^4.2.0",
|
|
80
86
|
"typescript": "^6.0.2"
|
|
81
87
|
},
|
package/dist/src/config.js
CHANGED
|
@@ -32,7 +32,11 @@ export const configSchema = z.object({
|
|
|
32
32
|
function getConfig() {
|
|
33
33
|
const result = configSchema.safeParse(process.env);
|
|
34
34
|
if (!result.success) {
|
|
35
|
-
|
|
35
|
+
const missing = result.error.issues.map(i => ` • ${i.path.join('.')}`).join('\n');
|
|
36
|
+
console.error(`\n❌ Missing or invalid environment variables:\n${missing}\n\n` +
|
|
37
|
+
`Set them in a .env file in the current directory, or export them before running.\n` +
|
|
38
|
+
`Required: ACTUAL_SERVER_URL, ACTUAL_PASSWORD, ACTUAL_BUDGET_SYNC_ID\n` +
|
|
39
|
+
`See: https://github.com/agigante80/actual-mcp-server\n`);
|
|
36
40
|
process.exit(1);
|
|
37
41
|
}
|
|
38
42
|
return result.data;
|
package/dist/src/index.js
CHANGED
|
@@ -80,6 +80,15 @@ if (argsEarly.includes('--stdio')) {
|
|
|
80
80
|
// Only load dotenv if we're not just showing help
|
|
81
81
|
// dotenv will be loaded inside the async IIFE below via dynamic import
|
|
82
82
|
// to avoid using require() in ESM and to keep the early --help fast exit.
|
|
83
|
+
const KNOWN_FLAGS = new Set([
|
|
84
|
+
'--http', '--stdio', '--test-actual-connection', '--test-mcp-client',
|
|
85
|
+
'--debug', '--help', '-h', '--version', '-v',
|
|
86
|
+
]);
|
|
87
|
+
const unknownFlags = argsEarly.filter(a => a.startsWith('-') && !KNOWN_FLAGS.has(a));
|
|
88
|
+
if (unknownFlags.length > 0) {
|
|
89
|
+
console.error(`Unknown option: ${unknownFlags.join(', ')}\nRun \`actual-mcp-server --help\` for usage.`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
83
92
|
if (argsEarly.includes('--help') || argsEarly.includes('-h') ||
|
|
84
93
|
argsEarly.includes('--version') || argsEarly.includes('-v')) {
|
|
85
94
|
const pkg = await import('../package.json', { with: { type: 'json' } });
|
|
@@ -333,7 +333,7 @@ export async function getAccounts() {
|
|
|
333
333
|
});
|
|
334
334
|
}
|
|
335
335
|
// addTransactions returns various formats: "ok", array of IDs, or Transaction objects
|
|
336
|
-
export async function addTransactions(txs) {
|
|
336
|
+
export async function addTransactions(txs, options = {}) {
|
|
337
337
|
observability.incrementToolCall('actual.transactions.create').catch(() => { });
|
|
338
338
|
return queueWriteOperation(async () => {
|
|
339
339
|
// The Actual API expects addTransactions(accountId, transactions, options)
|
|
@@ -352,7 +352,7 @@ export async function addTransactions(txs) {
|
|
|
352
352
|
return rest;
|
|
353
353
|
});
|
|
354
354
|
// API docs say it returns id[], but reality is it can return "ok", array of IDs, or Transaction objects
|
|
355
|
-
const result = await withConcurrency(() => retry(() => rawAddTransactions(accountId, cleanedTxs,
|
|
355
|
+
const result = await withConcurrency(() => retry(() => rawAddTransactions(accountId, cleanedTxs, options), { retries: 2, backoffMs: 200 }));
|
|
356
356
|
// Handle various return formats
|
|
357
357
|
if (result === 'ok') {
|
|
358
358
|
// Transaction created successfully but no IDs returned
|
|
@@ -383,6 +383,45 @@ export async function importTransactions(accountId, txs) {
|
|
|
383
383
|
return raw || { added: [], updated: [], errors: [] };
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
|
+
export async function createTransfer(params) {
|
|
387
|
+
observability.incrementToolCall('actual.transfers.create').catch(() => { });
|
|
388
|
+
return queueWriteOperation(async () => {
|
|
389
|
+
if (params.from_account === params.to_account) {
|
|
390
|
+
return { success: false, error: 'from_account and to_account must be different accounts.' };
|
|
391
|
+
}
|
|
392
|
+
const accounts = await withConcurrency(() => retry(() => rawGetAccounts(), { retries: 2, backoffMs: 200 }));
|
|
393
|
+
const fromAcc = accounts.find((a) => a.id === params.from_account);
|
|
394
|
+
const toAcc = accounts.find((a) => a.id === params.to_account);
|
|
395
|
+
if (!fromAcc)
|
|
396
|
+
return { success: false, error: `Account '${params.from_account}' not found. Use actual_accounts_list to find valid accounts.` };
|
|
397
|
+
if (fromAcc.closed)
|
|
398
|
+
return { success: false, error: `Source account '${fromAcc.name}' is closed.` };
|
|
399
|
+
if (!toAcc)
|
|
400
|
+
return { success: false, error: `Account '${params.to_account}' not found. Use actual_accounts_list to find valid accounts.` };
|
|
401
|
+
if (toAcc.closed)
|
|
402
|
+
return { success: false, error: `Destination account '${toAcc.name}' is closed.` };
|
|
403
|
+
const payees = await withConcurrency(() => retry(() => rawGetPayees(), { retries: 2, backoffMs: 200 }));
|
|
404
|
+
const transferPayee = payees.find((p) => p.transfer_acct === params.to_account && !p.tombstone);
|
|
405
|
+
if (!transferPayee) {
|
|
406
|
+
return { success: false, error: `No transfer payee found for destination account '${toAcc.name}'. The account may not support transfers.` };
|
|
407
|
+
}
|
|
408
|
+
const sourceTx = {
|
|
409
|
+
date: params.date,
|
|
410
|
+
amount: -Math.abs(params.amount),
|
|
411
|
+
payee: transferPayee.id,
|
|
412
|
+
...(params.notes !== undefined && { notes: params.notes }),
|
|
413
|
+
};
|
|
414
|
+
const result = await withConcurrency(() => retry(() => rawAddTransactions(params.from_account, [sourceTx], { runTransfers: true }), { retries: 2, backoffMs: 200 }));
|
|
415
|
+
let from_id = null;
|
|
416
|
+
if (Array.isArray(result) && result.length > 0 && typeof result[0] === 'string' && result[0] !== 'ok') {
|
|
417
|
+
from_id = result[0];
|
|
418
|
+
}
|
|
419
|
+
else if (result && typeof result === 'object' && 'id' in result) {
|
|
420
|
+
from_id = result.id;
|
|
421
|
+
}
|
|
422
|
+
return { success: true, from_id, to_id: null };
|
|
423
|
+
});
|
|
424
|
+
}
|
|
386
425
|
export async function getTransactions(accountId, startDate, endDate) {
|
|
387
426
|
return withActualApi(async () => {
|
|
388
427
|
observability.incrementToolCall('actual.transactions.get').catch(() => { });
|
|
@@ -1178,6 +1217,7 @@ export default {
|
|
|
1178
1217
|
getAccountsWithBalances,
|
|
1179
1218
|
addTransactions,
|
|
1180
1219
|
importTransactions,
|
|
1220
|
+
createTransfer,
|
|
1181
1221
|
getTransactions,
|
|
1182
1222
|
getCategories,
|
|
1183
1223
|
createCategory,
|
package/dist/src/tools/index.js
CHANGED
|
@@ -59,5 +59,6 @@ export { default as transactions_summary_by_payee } from './transactions_summary
|
|
|
59
59
|
export { default as transactions_uncategorized } from './transactions_uncategorized.js';
|
|
60
60
|
export { default as transactions_update } from './transactions_update.js';
|
|
61
61
|
export { default as transactions_update_batch } from './transactions_update_batch.js';
|
|
62
|
+
export { default as transfers_create } from './transfers_create.js';
|
|
62
63
|
export { default as session_close } from './session_close.js';
|
|
63
64
|
export { default as session_list } from './session_list.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createTool } from '../lib/toolFactory.js';
|
|
3
|
+
import { CommonSchemas } from '../lib/schemas/common.js';
|
|
4
|
+
import adapter from '../lib/actual-adapter.js';
|
|
5
|
+
const positiveAmountCents = z
|
|
6
|
+
.number()
|
|
7
|
+
.int('Amount must be an integer (cents)')
|
|
8
|
+
.positive('Amount must be a positive integer in cents (e.g. 5000 = $50.00)');
|
|
9
|
+
export default createTool({
|
|
10
|
+
name: 'actual_transfers_create',
|
|
11
|
+
description: 'Create a paired transfer between two accounts — a debit on the source account and a credit ' +
|
|
12
|
+
'on the destination account, linked by a shared transfer_id. Identical to the Actual Budget UI ' +
|
|
13
|
+
'"Make Transfer" result. Amount must be a positive integer in cents (e.g. 5000 = $50.00). ' +
|
|
14
|
+
'Both accounts must be open. Use actual_accounts_list to look up account UUIDs. ' +
|
|
15
|
+
'Note: if both accounts are bank-synced via GoCardless, a duplicate may appear after the next ' +
|
|
16
|
+
'sync if the bank settles the two sides on different dates.',
|
|
17
|
+
schema: z.object({
|
|
18
|
+
from_account: CommonSchemas.accountId.describe('UUID of the source account (money leaves this account)'),
|
|
19
|
+
to_account: CommonSchemas.accountId.describe('UUID of the destination account (money enters this account)'),
|
|
20
|
+
amount: positiveAmountCents,
|
|
21
|
+
date: CommonSchemas.date,
|
|
22
|
+
notes: z.string().max(1000).optional().describe('Optional memo visible on both sides of the transfer'),
|
|
23
|
+
}),
|
|
24
|
+
handler: async (input) => adapter.createTransfer(input),
|
|
25
|
+
examples: [
|
|
26
|
+
{
|
|
27
|
+
description: 'Transfer $50.00 from Checking to Credit Card',
|
|
28
|
+
input: { from_account: 'uuid-checking', to_account: 'uuid-cc', amount: 5000, date: '2024-01-15' },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
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.
|
|
4
|
+
"version": "0.5.6",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -58,13 +58,18 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@actual-app/api": "^26.4.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
|
+
"debug": "^4.4.3",
|
|
61
62
|
"dotenv": "^17.4.1",
|
|
62
63
|
"express": "^5.2.1",
|
|
64
|
+
"jose": "^6.1.3",
|
|
63
65
|
"mcp-auth": "^0.2.0",
|
|
64
66
|
"winston": "^3.18.3",
|
|
65
67
|
"winston-daily-rotate-file": "^5.0.0",
|
|
66
68
|
"zod": "^4.0.0"
|
|
67
69
|
},
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"prom-client": "*"
|
|
72
|
+
},
|
|
68
73
|
"overrides": {
|
|
69
74
|
"ajv": "8.18.0",
|
|
70
75
|
"qs": "6.14.2"
|
|
@@ -76,6 +81,7 @@
|
|
|
76
81
|
"@playwright/test": "^1.59.1",
|
|
77
82
|
"@types/express": "^5.0.3",
|
|
78
83
|
"@types/node": "^25.6.0",
|
|
84
|
+
"node-fetch": "^3.3.2",
|
|
79
85
|
"tsconfig-paths": "^4.2.0",
|
|
80
86
|
"typescript": "^6.0.2"
|
|
81
87
|
},
|