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 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) │ │ (77 tools) │ │ Server │
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) │ │ (77 tools) │ │ Server │
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
- - **77 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.
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 77 tools tested end-to-end. Any MCP-compatible client should work.
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 77 tools are available.
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
- **77 tools** across all categories. All tools use the `actual_<category>_<action>` naming convention.
311
+ **81 tools** across all categories. All tools use the `actual_<category>_<action>` naming convention.
312
312
 
313
- ### Accounts (8)
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 77 tools are available, identical to HTTP mode
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` | 77-tool smoke, schema validation, auth ACL | No |
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.20.0 | **Tool Count:** 77 (verified LibreChat-compatible)
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 77 tool definitions.
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
+ });
@@ -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.20.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 77 tools for AI-driven financial management with Actual Budget. HTTP and stdio transports for LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
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",