actual-mcp-server 0.20.0 → 0.21.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 +14 -10
- package/dist/src/actualToolsManager.js +4 -0
- package/dist/src/lib/actual-adapter.js +64 -1
- package/dist/src/lib/actual-schema.js +13 -0
- package/dist/src/lib/schemas/common.js +10 -1
- package/dist/src/lib/tool-annotations.js +4 -0
- package/dist/src/tools/account_groups_create.js +26 -0
- package/dist/src/tools/account_groups_delete.js +23 -0
- package/dist/src/tools/account_groups_list.js +23 -0
- package/dist/src/tools/account_groups_update.js +25 -0
- package/dist/src/tools/index.js +4 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/
|
|
|
24
24
|
┌─────────────┐ MCP/HTTP ┌──────────────────┐ Actual API ┌──────────────┐
|
|
25
25
|
│ LibreChat │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
|
|
26
26
|
│ LobeChat │ │ Server │ │ Budget │
|
|
27
|
-
│ (remote) │ │ (
|
|
27
|
+
│ (remote) │ │ (81 tools) │ │ Server │
|
|
28
28
|
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
29
29
|
|
|
30
30
|
┌─────────────┐ MCP/stdio ┌──────────────────┐ Actual API ┌──────────────┐
|
|
31
31
|
│ Claude │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
|
|
32
32
|
│ Desktop │ │ Server │ │ Budget │
|
|
33
|
-
│ (local) │ │ (
|
|
33
|
+
│ (local) │ │ (81 tools) │ │ Server │
|
|
34
34
|
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -38,14 +38,14 @@ Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/
|
|
|
38
38
|
|
|
39
39
|
Most Actual Budget MCP implementations are simple stdio bridges designed for single-user, local use with Claude Desktop. This project goes further:
|
|
40
40
|
|
|
41
|
-
- **
|
|
41
|
+
- **81 tools, the most comprehensive coverage available.** Accounts, transactions, categories, payees, tags, notes, rules, budgets, batch operations, bank sync, and more. Covers the reachable Actual Budget API with no genuine gaps.
|
|
42
42
|
- **HTTP and stdio transport.** Runs as a real remote server for LibreChat/LobeChat (`--http`), or as a direct local process for Claude Desktop (`--stdio`). No Docker or HTTP server is needed for local use.
|
|
43
43
|
- **6 exclusive ActualQL-powered tools.** Search and summarise transactions by month, amount, category, or payee using Actual Budget's native query engine. Aggregated results, no raw data dumped into the AI context window.
|
|
44
44
|
- **Multi-budget switching at runtime.** Configure multiple budget files and let the AI switch between them mid-conversation with `actual_budgets_switch`. Works on both transports: HTTP keys the active budget to the MCP session, and stdio (Claude Desktop, Claude Code, Cursor) gets a synthetic per-process session so a switch is scoped to that process rather than shared globally (#348).
|
|
45
45
|
- **Multi-user ready with OIDC.** Secure every session with JWKS-validated JWTs and per-user budget ACLs. No shared tokens required.
|
|
46
46
|
- **Production-grade reliability on both transports.** HTTP connection pooling (up to 15 concurrent sessions), and a long-lived stdio process that logs in ONCE and reuses that connection for every tool call instead of re-authenticating per call, so Claude Desktop and Claude Code stay fast and a burst of calls no longer risks a per-call login storm against the upstream limiter. Automatic retry with exponential backoff, and a full test suite (unit + E2E + integration).
|
|
47
47
|
|
|
48
|
-
> **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All
|
|
48
|
+
> **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All 81 tools tested end-to-end. Any MCP-compatible client should work.
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
@@ -182,7 +182,7 @@ Add to `claude_desktop_config.json` (see [docs/guides/MCP_CLIENTS_SETUP.md](docs
|
|
|
182
182
|
}
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
> **No token needed.** stdio runs as a local process owned by your user. The transport itself is the security boundary. All
|
|
185
|
+
> **No token needed.** stdio runs as a local process owned by your user. The transport itself is the security boundary. All 81 tools are available.
|
|
186
186
|
>
|
|
187
187
|
> **`MCP_BRIDGE_DATA_DIR` should be an absolute path.** Without one, the data directory resolves relative to wherever the client spawns the process, which can be unpredictable. The directory is created automatically on first run.
|
|
188
188
|
|
|
@@ -308,9 +308,9 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
308
308
|
|
|
309
309
|
## Available Tools
|
|
310
310
|
|
|
311
|
-
**
|
|
311
|
+
**81 tools** across all categories. All tools use the `actual_<category>_<action>` naming convention.
|
|
312
312
|
|
|
313
|
-
### Accounts (
|
|
313
|
+
### Accounts (12)
|
|
314
314
|
|
|
315
315
|
| Tool | Description |
|
|
316
316
|
|------|-------------|
|
|
@@ -322,6 +322,10 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
322
322
|
| `actual_accounts_reopen` | Reopen closed account. An id that is not an account is refused rather than creating one |
|
|
323
323
|
| `actual_accounts_get_balance` | Get account balance at a date |
|
|
324
324
|
| `actual_account_flow_summary` | Explain the exact balance change across a set of accounts over a date range: external income, expense outflow, credits, uncategorized inflows, and transfers into, out of, or within the selection, with an exact reconciliation |
|
|
325
|
+
| `actual_account_groups_list` | List the custom account groups used to organise accounts in the sidebar (Actual 26.9.0+) |
|
|
326
|
+
| `actual_account_groups_create` | Create an account group. Assigning accounts to it is a separate `actual_accounts_update` call |
|
|
327
|
+
| `actual_account_groups_update` | Rename an account group or change its sidebar position. A group that does not exist is refused, writing nothing |
|
|
328
|
+
| `actual_account_groups_delete` | Delete an account group. Its accounts are NOT deleted, they become ungrouped |
|
|
325
329
|
|
|
326
330
|
### Transactions (16)
|
|
327
331
|
|
|
@@ -594,7 +598,7 @@ stdio is the simplest way to connect Claude Desktop directly to Actual Budget. T
|
|
|
594
598
|
- No auth token. Process ownership is the security boundary.
|
|
595
599
|
- All logs go to stderr so they never corrupt the JSON-RPC framing on stdout
|
|
596
600
|
- The process exits when stdin closes (Claude Desktop shutting down)
|
|
597
|
-
- All
|
|
601
|
+
- All 81 tools are available, identical to HTTP mode
|
|
598
602
|
|
|
599
603
|
**Start manually to verify:**
|
|
600
604
|
|
|
@@ -693,7 +697,7 @@ See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-m
|
|
|
693
697
|
| Command | What It Tests | Requires Live Server |
|
|
694
698
|
|---------|---------------|---------------------|
|
|
695
699
|
| `npm run build` | TypeScript compilation | No |
|
|
696
|
-
| `npm run test:unit-js` |
|
|
700
|
+
| `npm run test:unit-js` | 81-tool smoke, schema validation, auth ACL | No |
|
|
697
701
|
| `npm run test:adapter` | Adapter, retry logic, concurrency | No |
|
|
698
702
|
| `npm run test:e2e` | MCP protocol compliance (Playwright) | No |
|
|
699
703
|
| `npm run test:e2e:docker:full` | Full stack integration | Yes (Docker) |
|
|
@@ -777,7 +781,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
777
781
|
|
|
778
782
|
---
|
|
779
783
|
|
|
780
|
-
**Version:** 0.
|
|
784
|
+
**Version:** 0.21.0 | **Tool Count:** 81 (verified LibreChat-compatible)
|
|
781
785
|
|
|
782
786
|
## Sponsor
|
|
783
787
|
|
|
@@ -10,6 +10,10 @@ const IMPLEMENTED_TOOLS = [
|
|
|
10
10
|
'actual_accounts_create',
|
|
11
11
|
'actual_accounts_delete',
|
|
12
12
|
'actual_accounts_get_balance',
|
|
13
|
+
'actual_account_groups_list',
|
|
14
|
+
'actual_account_groups_create',
|
|
15
|
+
'actual_account_groups_update',
|
|
16
|
+
'actual_account_groups_delete',
|
|
13
17
|
'actual_accounts_list',
|
|
14
18
|
'actual_accounts_reopen',
|
|
15
19
|
'actual_accounts_update',
|
|
@@ -5,7 +5,7 @@ import api from '@actual-app/api';
|
|
|
5
5
|
// cannot expose its named exports via static import syntax. At runtime the default import
|
|
6
6
|
// IS module.exports, so all methods are accessible as properties.
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
-
const { addTransactions: rawAddTransactions, getAccounts: rawGetAccounts, importTransactions: rawImportTransactions, getTransactions: rawGetTransactions, getCategories: rawGetCategories, createCategory: rawCreateCategory, getPayees: rawGetPayees, getCommonPayees: rawGetCommonPayees, createPayee: rawCreatePayee, getBudgetMonths: rawGetBudgetMonths, getBudgetMonth: rawGetBudgetMonth, setBudgetAmount: rawSetBudgetAmount, createAccount: rawCreateAccount, updateAccount: rawUpdateAccount, getAccountBalance: rawGetAccountBalance, updateTransaction: rawUpdateTransaction, deleteTransaction: rawDeleteTransaction, updateCategory: rawUpdateCategory, deleteCategory: rawDeleteCategory, updatePayee: rawUpdatePayee, deletePayee: rawDeletePayee, deleteAccount: rawDeleteAccount, getRules: rawGetRules, createRule: rawCreateRule, updateRule: rawUpdateRule, deleteRule: rawDeleteRule, setBudgetCarryover: rawSetBudgetCarryover, closeAccount: rawCloseAccount, reopenAccount: rawReopenAccount, getCategoryGroups: rawGetCategoryGroups, createCategoryGroup: rawCreateCategoryGroup, updateCategoryGroup: rawUpdateCategoryGroup, deleteCategoryGroup: rawDeleteCategoryGroup, mergePayees: rawMergePayees, getPayeeRules: rawGetPayeeRules, batchBudgetUpdates: rawBatchBudgetUpdates, holdBudgetForNextMonth: rawHoldBudgetForNextMonth, resetBudgetHold: rawResetBudgetHold, runQuery: rawRunQuery, runBankSync: rawRunBankSync, getBudgets: rawGetBudgets, getIDByName: rawGetIDByName, getServerVersion: rawGetServerVersion, getSchedules: rawGetSchedules, createSchedule: rawCreateSchedule, updateSchedule: rawUpdateSchedule, deleteSchedule: rawDeleteSchedule, getTags: rawGetTags, createTag: rawCreateTag, updateTag: rawUpdateTag, deleteTag: rawDeleteTag, getNote: rawGetNote, updateNote: rawUpdateNote, exportBudget: rawExportBudget, importBudget: rawImportBudget, getPreferences: rawGetPreferences,
|
|
8
|
+
const { addTransactions: rawAddTransactions, getAccounts: rawGetAccounts, importTransactions: rawImportTransactions, getTransactions: rawGetTransactions, getCategories: rawGetCategories, createCategory: rawCreateCategory, getPayees: rawGetPayees, getCommonPayees: rawGetCommonPayees, createPayee: rawCreatePayee, getBudgetMonths: rawGetBudgetMonths, getBudgetMonth: rawGetBudgetMonth, setBudgetAmount: rawSetBudgetAmount, createAccount: rawCreateAccount, updateAccount: rawUpdateAccount, getAccountBalance: rawGetAccountBalance, updateTransaction: rawUpdateTransaction, deleteTransaction: rawDeleteTransaction, updateCategory: rawUpdateCategory, deleteCategory: rawDeleteCategory, updatePayee: rawUpdatePayee, deletePayee: rawDeletePayee, deleteAccount: rawDeleteAccount, getRules: rawGetRules, createRule: rawCreateRule, updateRule: rawUpdateRule, deleteRule: rawDeleteRule, setBudgetCarryover: rawSetBudgetCarryover, closeAccount: rawCloseAccount, reopenAccount: rawReopenAccount, getCategoryGroups: rawGetCategoryGroups, createCategoryGroup: rawCreateCategoryGroup, updateCategoryGroup: rawUpdateCategoryGroup, deleteCategoryGroup: rawDeleteCategoryGroup, mergePayees: rawMergePayees, getPayeeRules: rawGetPayeeRules, batchBudgetUpdates: rawBatchBudgetUpdates, holdBudgetForNextMonth: rawHoldBudgetForNextMonth, resetBudgetHold: rawResetBudgetHold, runQuery: rawRunQuery, runBankSync: rawRunBankSync, getBudgets: rawGetBudgets, getIDByName: rawGetIDByName, getServerVersion: rawGetServerVersion, getSchedules: rawGetSchedules, createSchedule: rawCreateSchedule, updateSchedule: rawUpdateSchedule, deleteSchedule: rawDeleteSchedule, getTags: rawGetTags, createTag: rawCreateTag, updateTag: rawUpdateTag, deleteTag: rawDeleteTag, getNote: rawGetNote, updateNote: rawUpdateNote, exportBudget: rawExportBudget, importBudget: rawImportBudget, getPreferences: rawGetPreferences, getAccountGroups: rawGetAccountGroups, createAccountGroup: rawCreateAccountGroup, updateAccountGroup: rawUpdateAccountGroup, deleteAccountGroup: rawDeleteAccountGroup,
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
10
|
} = api;
|
|
11
11
|
import { EventEmitter } from 'events';
|
|
@@ -3200,6 +3200,65 @@ export async function getServerVersion() {
|
|
|
3200
3200
|
});
|
|
3201
3201
|
}
|
|
3202
3202
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3203
|
+
/**
|
|
3204
|
+
* Account groups (#429). Upstream added these in 26.9.0; the schema arrives through a
|
|
3205
|
+
* migration SHIPPED IN `@actual-app/api` and applied to the LOCAL budget file, so these
|
|
3206
|
+
* work against an older sync server too: the server relays messages and does not execute
|
|
3207
|
+
* the queries.
|
|
3208
|
+
*
|
|
3209
|
+
* One semantic worth carrying into the tool descriptions, taken from upstream's own
|
|
3210
|
+
* comment on the delete path: clearing member refs is best effort under CRDT sync, so a
|
|
3211
|
+
* concurrent assignment on another device can win against those nulls. Consumers must
|
|
3212
|
+
* treat an account whose `account_group_id` points at a missing or tombstoned group as
|
|
3213
|
+
* UNGROUPED rather than as an error.
|
|
3214
|
+
*/
|
|
3215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3216
|
+
export async function getAccountGroups() {
|
|
3217
|
+
return withActualApi(async () => {
|
|
3218
|
+
observability.incrementToolCall('actual.account_groups.list').catch(() => { });
|
|
3219
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3220
|
+
return await withConcurrency(() => retry(() => rawGetAccountGroups(), { retries: 2, backoffMs: 200 }));
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
export async function createAccountGroup(group) {
|
|
3224
|
+
observability.incrementToolCall('actual.account_groups.create').catch(() => { });
|
|
3225
|
+
return queueWriteOperation(async () => {
|
|
3226
|
+
const raw = await withConcurrency(() => retry(() => rawCreateAccountGroup(group), { retries: 2, backoffMs: 200, isRetryable: isRetryableError }));
|
|
3227
|
+
return normalizeToId(raw);
|
|
3228
|
+
// Lands in account_groups only, so the four entity listings are untouched.
|
|
3229
|
+
}, { preservesListings: PRESERVES_ALL_ENTITY_LISTINGS });
|
|
3230
|
+
}
|
|
3231
|
+
export async function updateAccountGroup(id, fields) {
|
|
3232
|
+
observability.incrementToolCall('actual.account_groups.update').catch(() => { });
|
|
3233
|
+
return queueWriteOperation(async () => {
|
|
3234
|
+
// Read, decide and write inside ONE queued operation, so the existence check and the
|
|
3235
|
+
// write are a single api-lock cycle and cannot be interleaved by a sibling (#371/#378).
|
|
3236
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3237
|
+
const groups = await withConcurrency(() => retry(() => rawGetAccountGroups(), { retries: 2, backoffMs: 200 }));
|
|
3238
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3239
|
+
if (!groups.some((g) => g.id === id)) {
|
|
3240
|
+
throw new NotFoundRefusal('Account group', id, 'actual_account_groups_list');
|
|
3241
|
+
}
|
|
3242
|
+
await withConcurrency(() => retry(() => rawUpdateAccountGroup(id, fields), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
3243
|
+
// Renames the group row itself; no account row changes, so listings are preserved.
|
|
3244
|
+
}, { preservesListings: PRESERVES_ALL_ENTITY_LISTINGS });
|
|
3245
|
+
}
|
|
3246
|
+
export async function deleteAccountGroup(id) {
|
|
3247
|
+
observability.incrementToolCall('actual.account_groups.delete').catch(() => { });
|
|
3248
|
+
return queueWriteOperation(async () => {
|
|
3249
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3250
|
+
const groups = await withConcurrency(() => retry(() => rawGetAccountGroups(), { retries: 2, backoffMs: 200 }));
|
|
3251
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3252
|
+
if (!groups.some((g) => g.id === id)) {
|
|
3253
|
+
throw new NotFoundRefusal('Account group', id, 'actual_account_groups_list');
|
|
3254
|
+
}
|
|
3255
|
+
await withConcurrency(() => retry(() => rawDeleteAccountGroup(id), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
3256
|
+
// NO preservesListings claim, deliberately: upstream's delete UPDATES every member
|
|
3257
|
+
// account to null its account_group_id before removing the group, so the accounts
|
|
3258
|
+
// listing CONTENT changes even though its id set does not. Claiming preservation here
|
|
3259
|
+
// would serve a stale accounts listing to the next guard in the same drain.
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3203
3262
|
export async function getTags() {
|
|
3204
3263
|
return withActualApi(async () => {
|
|
3205
3264
|
observability.incrementToolCall('actual.tags.get').catch(() => { });
|
|
@@ -3421,6 +3480,10 @@ export default {
|
|
|
3421
3480
|
getPayees,
|
|
3422
3481
|
getCommonPayees,
|
|
3423
3482
|
createPayee,
|
|
3483
|
+
getAccountGroups,
|
|
3484
|
+
createAccountGroup,
|
|
3485
|
+
updateAccountGroup,
|
|
3486
|
+
deleteAccountGroup,
|
|
3424
3487
|
getTags,
|
|
3425
3488
|
createTag,
|
|
3426
3489
|
updateTag,
|
|
@@ -52,6 +52,19 @@ export const ACTUAL_SCHEMA = {
|
|
|
52
52
|
mask: { type: 'string' },
|
|
53
53
|
bank: { type: 'id', ref: 'banks' },
|
|
54
54
|
account_sync_source: { type: 'string' },
|
|
55
|
+
// #429: these four exist in upstream's ActualQL schema but were missing here, so
|
|
56
|
+
// actual_query_run rejected valid queries against them. account_group_id arrived with
|
|
57
|
+
// account groups in 26.9.0; the other three predate it and were simply never added.
|
|
58
|
+
account_group_id: { type: 'id', ref: 'account_groups' },
|
|
59
|
+
last_reconciled: { type: 'string' },
|
|
60
|
+
last_sync: { type: 'string' },
|
|
61
|
+
bank_sync_status: { type: 'string' },
|
|
62
|
+
},
|
|
63
|
+
account_groups: {
|
|
64
|
+
id: { type: 'id' },
|
|
65
|
+
name: { type: 'string', required: true },
|
|
66
|
+
sort_order: { type: 'float' },
|
|
67
|
+
tombstone: { type: 'boolean' },
|
|
55
68
|
},
|
|
56
69
|
categories: {
|
|
57
70
|
id: { type: 'id' },
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Common validation schemas used across MCP tools for Actual Budget.
|
|
5
5
|
* These schemas provide consistent validation, better error messages,
|
|
6
|
-
* and reduce duplication across the
|
|
6
|
+
* and reduce duplication across the 81 tool definitions.
|
|
7
7
|
*/
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { MAX_NAME_LENGTH, MAX_NOTES_LENGTH, DATE_PATTERN, MONTH_PATTERN, UUID_PATTERN } from '../constants.js';
|
|
@@ -62,6 +62,14 @@ export const ruleIdSchema = z
|
|
|
62
62
|
* Tag UUID validation
|
|
63
63
|
* Used for: tag management operations (update, delete)
|
|
64
64
|
*/
|
|
65
|
+
/**
|
|
66
|
+
* Account group UUID validation (#429)
|
|
67
|
+
* Used for: account group management operations (update, delete)
|
|
68
|
+
*/
|
|
69
|
+
export const accountGroupIdSchema = z
|
|
70
|
+
.string()
|
|
71
|
+
.regex(UUID_PATTERN, 'Invalid account group ID format (expected UUID)')
|
|
72
|
+
.describe('Account group UUID');
|
|
65
73
|
export const tagIdSchema = z
|
|
66
74
|
.string()
|
|
67
75
|
.regex(UUID_PATTERN, 'Invalid tag ID format (expected UUID)')
|
|
@@ -225,6 +233,7 @@ export const CommonSchemas = {
|
|
|
225
233
|
categoryGroupId: categoryGroupIdSchema,
|
|
226
234
|
payeeId: payeeIdSchema,
|
|
227
235
|
ruleId: ruleIdSchema,
|
|
236
|
+
accountGroupId: accountGroupIdSchema,
|
|
228
237
|
tagId: tagIdSchema,
|
|
229
238
|
scheduleId: scheduleIdSchema,
|
|
230
239
|
// Dates
|
|
@@ -55,6 +55,7 @@ const OPEN_WORLD = new Set(['actual_bank_sync']);
|
|
|
55
55
|
* actual_session_close closes a pooled connection: server state, not budget data.
|
|
56
56
|
*/
|
|
57
57
|
const READ_ONLY = new Set([
|
|
58
|
+
'actual_account_groups_list',
|
|
58
59
|
'actual_account_flow_summary',
|
|
59
60
|
'actual_recurring_expenses_summary',
|
|
60
61
|
'actual_accounts_get_balance',
|
|
@@ -99,6 +100,7 @@ const READ_ONLY = new Set([
|
|
|
99
100
|
* and in #357), so a close can be a delete.
|
|
100
101
|
*/
|
|
101
102
|
const DESTRUCTIVE = new Set([
|
|
103
|
+
'actual_account_groups_delete',
|
|
102
104
|
'actual_accounts_close',
|
|
103
105
|
'actual_accounts_delete',
|
|
104
106
|
// Upstream `reconcileTransactions` overwrites fields on transactions that ALREADY exist
|
|
@@ -151,6 +153,8 @@ const DESTRUCTIVE = new Set([
|
|
|
151
153
|
* not, because on the split path it deletes and recreates child rows.
|
|
152
154
|
*/
|
|
153
155
|
const ADDITIVE = new Set([
|
|
156
|
+
'actual_account_groups_create',
|
|
157
|
+
'actual_account_groups_update',
|
|
154
158
|
'actual_accounts_create',
|
|
155
159
|
'actual_accounts_reopen',
|
|
156
160
|
'actual_accounts_update',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createTool } from '../lib/toolFactory.js';
|
|
3
|
+
import adapter from '../lib/actual-adapter.js';
|
|
4
|
+
/** #429: create a custom account group (Actual 26.9.0+). */
|
|
5
|
+
export default createTool({
|
|
6
|
+
name: 'actual_account_groups_create',
|
|
7
|
+
description: 'Create a custom account group for organising accounts in the sidebar. ' +
|
|
8
|
+
'Returns the id of the new group. Assigning accounts to it is a separate step: ' +
|
|
9
|
+
'set the account\'s account_group_id with actual_accounts_update. ' +
|
|
10
|
+
'Requires an Actual server on 26.9.0 or newer.',
|
|
11
|
+
schema: z.object({
|
|
12
|
+
name: z.string().min(1).describe('Display name for the group, for example "Savings" or "Joint accounts"'),
|
|
13
|
+
sort_order: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Optional ordering position in the sidebar. Lower sorts first; omit to let Actual place it.'),
|
|
17
|
+
}),
|
|
18
|
+
handler: async (input) => {
|
|
19
|
+
const id = await adapter.createAccountGroup(input);
|
|
20
|
+
return { id, name: input.name };
|
|
21
|
+
},
|
|
22
|
+
examples: [
|
|
23
|
+
{ description: 'Create a group for savings accounts', input: { name: 'Savings' } },
|
|
24
|
+
{ description: 'Create a group pinned to the top of the sidebar', input: { name: 'Daily spending', sort_order: 0 } },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
/** #429: delete a custom account group (Actual 26.9.0+). */
|
|
6
|
+
export default createTool({
|
|
7
|
+
name: 'actual_account_groups_delete',
|
|
8
|
+
description: 'Delete a custom account group. The accounts in it are NOT deleted: they become ungrouped. ' +
|
|
9
|
+
'Note that upstream clears those member references best-effort under sync, so a concurrent ' +
|
|
10
|
+
'assignment on another device can survive the delete; treat any account still pointing at a ' +
|
|
11
|
+
'missing group as ungrouped. Refuses with a not-found error, writing nothing, when the group ' +
|
|
12
|
+
'does not exist. Requires an Actual server on 26.9.0 or newer.',
|
|
13
|
+
schema: z.object({
|
|
14
|
+
id: CommonSchemas.accountGroupId.describe('Id of the account group to delete (actual_account_groups_list lists them)'),
|
|
15
|
+
}),
|
|
16
|
+
handler: async (input) => {
|
|
17
|
+
await adapter.deleteAccountGroup(input.id);
|
|
18
|
+
return { success: true, id: input.id };
|
|
19
|
+
},
|
|
20
|
+
examples: [
|
|
21
|
+
{ description: 'Delete a group, leaving its accounts ungrouped', input: { id: '00000000-0000-0000-0000-000000000000' } },
|
|
22
|
+
],
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createTool } from '../lib/toolFactory.js';
|
|
3
|
+
import adapter from '../lib/actual-adapter.js';
|
|
4
|
+
/**
|
|
5
|
+
* #429: list the custom account groups (Actual 26.9.0+).
|
|
6
|
+
*
|
|
7
|
+
* Read the grouping semantics before relying on a group id: upstream clears member
|
|
8
|
+
* references on a best-effort basis under CRDT sync, so an account whose
|
|
9
|
+
* `account_group_id` points at a group that is missing here is UNGROUPED, not an error.
|
|
10
|
+
*/
|
|
11
|
+
export default createTool({
|
|
12
|
+
name: 'actual_account_groups_list',
|
|
13
|
+
description: 'List the custom account groups used to organise accounts in the sidebar. ' +
|
|
14
|
+
'Returns each group with its id, name and sort order. An account whose account_group_id ' +
|
|
15
|
+
'does not appear here is ungrouped: upstream clears member references best-effort under ' +
|
|
16
|
+
'sync, so a dangling reference is expected and is not an error. Requires an Actual server ' +
|
|
17
|
+
'on 26.9.0 or newer, which is where account groups were introduced.',
|
|
18
|
+
schema: z.object({}),
|
|
19
|
+
handler: async () => {
|
|
20
|
+
return await adapter.getAccountGroups();
|
|
21
|
+
},
|
|
22
|
+
examples: [{ description: 'List every account group', input: {} }],
|
|
23
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
/** #429: rename or reorder a custom account group (Actual 26.9.0+). */
|
|
6
|
+
export default createTool({
|
|
7
|
+
name: 'actual_account_groups_update',
|
|
8
|
+
description: 'Rename a custom account group or change its sidebar position. ' +
|
|
9
|
+
'Does not change which accounts belong to it: use actual_accounts_update to move an ' +
|
|
10
|
+
'account between groups. Refuses with a not-found error, writing nothing, when the group ' +
|
|
11
|
+
'does not exist. Requires an Actual server on 26.9.0 or newer.',
|
|
12
|
+
schema: z.object({
|
|
13
|
+
id: CommonSchemas.accountGroupId.describe('Id of the account group to update (actual_account_groups_list lists them)'),
|
|
14
|
+
name: z.string().min(1).optional().describe('New display name'),
|
|
15
|
+
sort_order: z.number().optional().describe('New ordering position in the sidebar; lower sorts first'),
|
|
16
|
+
}),
|
|
17
|
+
handler: async (input) => {
|
|
18
|
+
const { id, ...fields } = input;
|
|
19
|
+
await adapter.updateAccountGroup(id, fields);
|
|
20
|
+
return { success: true, id, updated: Object.keys(fields) };
|
|
21
|
+
},
|
|
22
|
+
examples: [
|
|
23
|
+
{ description: 'Rename a group', input: { id: '00000000-0000-0000-0000-000000000000', name: 'Long-term savings' } },
|
|
24
|
+
],
|
|
25
|
+
});
|
package/dist/src/tools/index.js
CHANGED
|
@@ -6,6 +6,10 @@ export { default as accounts_close } from './accounts_close.js';
|
|
|
6
6
|
export { default as accounts_create } from './accounts_create.js';
|
|
7
7
|
export { default as accounts_delete } from './accounts_delete.js';
|
|
8
8
|
export { default as accounts_get_balance } from './accounts_get_balance.js';
|
|
9
|
+
export { default as account_groups_list } from './account_groups_list.js';
|
|
10
|
+
export { default as account_groups_create } from './account_groups_create.js';
|
|
11
|
+
export { default as account_groups_update } from './account_groups_update.js';
|
|
12
|
+
export { default as account_groups_delete } from './account_groups_delete.js';
|
|
9
13
|
export { default as accounts_list } from './accounts_list.js';
|
|
10
14
|
export { default as accounts_reopen } from './accounts_reopen.js';
|
|
11
15
|
export { default as accounts_update } from './accounts_update.js';
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.21.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
8
8
|
},
|
|
9
|
-
"description": "MCP server with
|
|
9
|
+
"description": "MCP server with 81 tools for AI-driven financial management with Actual Budget. HTTP and stdio transports for LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
|
|
10
10
|
"homepage": "https://github.com/agigante80/actual-mcp-server#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
31
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
32
|
"direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
|
|
33
|
-
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js",
|
|
34
34
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
35
|
"test:e2e": "npx playwright test",
|
|
36
36
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|