actual-mcp-server 0.5.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +663 -0
  3. package/bin/actual-mcp-server.js +3 -0
  4. package/dist/generated/actual-client/types.js +5 -0
  5. package/dist/package.json +88 -0
  6. package/dist/src/actualConnection.js +157 -0
  7. package/dist/src/actualToolsManager.js +211 -0
  8. package/dist/src/auth/budget-acl.js +143 -0
  9. package/dist/src/auth/setup.js +58 -0
  10. package/dist/src/config.js +41 -0
  11. package/dist/src/index.js +313 -0
  12. package/dist/src/lib/ActualConnectionPool.js +343 -0
  13. package/dist/src/lib/ActualMCPConnection.js +125 -0
  14. package/dist/src/lib/actual-adapter.js +1228 -0
  15. package/dist/src/lib/actual-schema.js +222 -0
  16. package/dist/src/lib/budget-registry.js +64 -0
  17. package/dist/src/lib/constants.js +121 -0
  18. package/dist/src/lib/errors.js +19 -0
  19. package/dist/src/lib/loggerFactory.js +72 -0
  20. package/dist/src/lib/node-polyfills.js +20 -0
  21. package/dist/src/lib/query-validator.js +221 -0
  22. package/dist/src/lib/retry.js +26 -0
  23. package/dist/src/lib/schemas/common.js +203 -0
  24. package/dist/src/lib/toolFactory.js +109 -0
  25. package/dist/src/logger.js +127 -0
  26. package/dist/src/observability.js +58 -0
  27. package/dist/src/prompts/showLargeTransactions.js +6 -0
  28. package/dist/src/resources/accountsSummary.js +13 -0
  29. package/dist/src/server/httpServer.js +540 -0
  30. package/dist/src/server/httpServer_testing.js +401 -0
  31. package/dist/src/server/stdioServer.js +52 -0
  32. package/dist/src/server/streamable-http.js +148 -0
  33. package/dist/src/tests/actualToolsTests.js +70 -0
  34. package/dist/src/tests/observability.smoke.test.js +18 -0
  35. package/dist/src/tests/testMcpClient.js +170 -0
  36. package/dist/src/tests_adapter_runner.js +86 -0
  37. package/dist/src/tools/accounts_close.js +16 -0
  38. package/dist/src/tools/accounts_create.js +27 -0
  39. package/dist/src/tools/accounts_delete.js +16 -0
  40. package/dist/src/tools/accounts_get_balance.js +40 -0
  41. package/dist/src/tools/accounts_list.js +16 -0
  42. package/dist/src/tools/accounts_reopen.js +16 -0
  43. package/dist/src/tools/accounts_update.js +52 -0
  44. package/dist/src/tools/bank_sync.js +22 -0
  45. package/dist/src/tools/budget_updates_batch.js +77 -0
  46. package/dist/src/tools/budgets_getMonth.js +14 -0
  47. package/dist/src/tools/budgets_getMonths.js +14 -0
  48. package/dist/src/tools/budgets_get_all.js +13 -0
  49. package/dist/src/tools/budgets_holdForNextMonth.js +19 -0
  50. package/dist/src/tools/budgets_list_available.js +20 -0
  51. package/dist/src/tools/budgets_resetHold.js +16 -0
  52. package/dist/src/tools/budgets_setAmount.js +26 -0
  53. package/dist/src/tools/budgets_setCarryover.js +18 -0
  54. package/dist/src/tools/budgets_switch.js +27 -0
  55. package/dist/src/tools/budgets_transfer.js +64 -0
  56. package/dist/src/tools/categories_create.js +65 -0
  57. package/dist/src/tools/categories_delete.js +16 -0
  58. package/dist/src/tools/categories_get.js +14 -0
  59. package/dist/src/tools/categories_update.js +22 -0
  60. package/dist/src/tools/category_groups_create.js +18 -0
  61. package/dist/src/tools/category_groups_delete.js +26 -0
  62. package/dist/src/tools/category_groups_get.js +13 -0
  63. package/dist/src/tools/category_groups_update.js +21 -0
  64. package/dist/src/tools/get_id_by_name.js +36 -0
  65. package/dist/src/tools/index.js +63 -0
  66. package/dist/src/tools/payee_rules_get.js +27 -0
  67. package/dist/src/tools/payees_create.js +25 -0
  68. package/dist/src/tools/payees_delete.js +16 -0
  69. package/dist/src/tools/payees_get.js +14 -0
  70. package/dist/src/tools/payees_merge.js +17 -0
  71. package/dist/src/tools/payees_update.js +59 -0
  72. package/dist/src/tools/query_run.js +78 -0
  73. package/dist/src/tools/rules_create.js +129 -0
  74. package/dist/src/tools/rules_create_or_update.js +191 -0
  75. package/dist/src/tools/rules_delete.js +26 -0
  76. package/dist/src/tools/rules_get.js +13 -0
  77. package/dist/src/tools/rules_update.js +120 -0
  78. package/dist/src/tools/schedules_create.js +54 -0
  79. package/dist/src/tools/schedules_delete.js +41 -0
  80. package/dist/src/tools/schedules_get.js +13 -0
  81. package/dist/src/tools/schedules_update.js +40 -0
  82. package/dist/src/tools/server_get_version.js +22 -0
  83. package/dist/src/tools/server_info.js +86 -0
  84. package/dist/src/tools/session_close.js +100 -0
  85. package/dist/src/tools/session_list.js +24 -0
  86. package/dist/src/tools/transactions_create.js +50 -0
  87. package/dist/src/tools/transactions_delete.js +20 -0
  88. package/dist/src/tools/transactions_filter.js +73 -0
  89. package/dist/src/tools/transactions_get.js +23 -0
  90. package/dist/src/tools/transactions_import.js +21 -0
  91. package/dist/src/tools/transactions_search_by_amount.js +126 -0
  92. package/dist/src/tools/transactions_search_by_category.js +137 -0
  93. package/dist/src/tools/transactions_search_by_month.js +142 -0
  94. package/dist/src/tools/transactions_search_by_payee.js +142 -0
  95. package/dist/src/tools/transactions_summary_by_category.js +80 -0
  96. package/dist/src/tools/transactions_summary_by_payee.js +72 -0
  97. package/dist/src/tools/transactions_uncategorized.js +66 -0
  98. package/dist/src/tools/transactions_update.js +34 -0
  99. package/dist/src/tools/transactions_update_batch.js +60 -0
  100. package/dist/src/utils.js +63 -0
  101. package/package.json +88 -0
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, 'month must be in YYYY-MM format').describe('Budget month in YYYY-MM format'),
5
+ });
6
+ const tool = {
7
+ name: 'actual_budgets_resetHold',
8
+ description: `Reset the budget hold for a month, releasing any amount previously held for next month via holdBudgetForNextMonth.`,
9
+ inputSchema: InputSchema,
10
+ call: async (args, _meta) => {
11
+ const input = InputSchema.parse(args || {});
12
+ await adapter.resetBudgetHold(input.month);
13
+ return { success: true };
14
+ },
15
+ };
16
+ export default tool;
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({ month: z.string().min(1), categoryId: z.string().min(1), amount: z.number() });
4
+ const tool = {
5
+ name: 'actual_budgets_setAmount',
6
+ description: "Set the budgeted amount for a specific category in a given month. Amount in cents (e.g., 50000 = $500). Use this to allocate money to spending categories for budget planning.",
7
+ inputSchema: InputSchema,
8
+ call: async (args, _meta) => {
9
+ const input = InputSchema.parse(args || {});
10
+ try {
11
+ const result = await adapter.setBudgetAmount(input.month, input.categoryId, input.amount);
12
+ return { result };
13
+ }
14
+ catch (error) {
15
+ const msg = error instanceof Error ? error.message : String(error);
16
+ // The pre-flight in adapter.setBudgetAmount throws:
17
+ // Category "${categoryId}" not found. Use actual_categories_get to list available categories.
18
+ // That string always contains BOTH "not found" AND "category", so && is the correct operator.
19
+ if (msg.toLowerCase().includes('not found') && msg.toLowerCase().includes('category')) {
20
+ return { success: false, error: msg };
21
+ }
22
+ throw new Error(`Failed to set budget amount: ${msg}`);
23
+ }
24
+ },
25
+ };
26
+ export default tool;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ month: z.string().describe('Budget month in YYYY-MM format'),
5
+ categoryId: z.string().describe('Category ID'),
6
+ flag: z.boolean().describe('Whether to carry over unused budget to next month (true) or not (false)'),
7
+ });
8
+ const tool = {
9
+ name: 'actual_budgets_setCarryover',
10
+ description: `Set carryover behavior for a category in a specific month. When enabled (true), leftover budget or overspending automatically rolls into the next month's available balance. When disabled (false), each month starts fresh with no carryover - useful for fixed monthly expenses. Essential for flexible budget management.`,
11
+ inputSchema: InputSchema,
12
+ call: async (args, _meta) => {
13
+ const input = InputSchema.parse(args || {});
14
+ await adapter.setBudgetCarryover(input.month, input.categoryId, input.flag);
15
+ return { success: true };
16
+ },
17
+ };
18
+ export default tool;
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ budgetName: z.string().min(1, 'budgetName must not be empty').describe('The name of the budget to switch to, as configured via BUDGET_n_NAME environment variables. ' +
5
+ 'Use actual_budgets_list_available to see all available budget names. ' +
6
+ 'Partial / case-insensitive match is supported (e.g. "office" matches "Office Budget").'),
7
+ });
8
+ const tool = {
9
+ name: 'actual_budgets_switch',
10
+ description: 'Switch to a different pre-configured budget for all subsequent operations. ' +
11
+ 'Each budget can target a different Actual Budget server, sync ID, and encryption password. ' +
12
+ 'Call actual_budgets_list_available first to see the available budget names. ' +
13
+ 'The switch is instant and takes effect immediately — no server restart required.',
14
+ inputSchema: InputSchema,
15
+ call: async (args) => {
16
+ const { budgetName } = InputSchema.parse(args);
17
+ const result = await Promise.resolve(adapter.switchBudget(budgetName));
18
+ return {
19
+ success: true,
20
+ budgetName: result.name,
21
+ budgetId: result.syncId,
22
+ serverUrl: result.serverUrl,
23
+ message: `Switched to budget '${result.name}' (${result.syncId}) on ${result.serverUrl}. All subsequent operations now target this budget.`,
24
+ };
25
+ },
26
+ };
27
+ export default tool;
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ month: z.string().describe('Month in YYYY-MM format'),
5
+ fromCategoryId: z.string().describe('Source category ID to transfer from'),
6
+ toCategoryId: z.string().describe('Target category ID to transfer to'),
7
+ amount: z.number().describe('Amount to transfer in cents (positive integer)'),
8
+ });
9
+ const tool = {
10
+ name: 'actual_budgets_transfer',
11
+ description: 'Transfer budget amount between categories. Decreases the source category budget and increases the target category budget by the specified amount for the given month.',
12
+ inputSchema: InputSchema,
13
+ call: async (args, _meta) => {
14
+ const input = InputSchema.parse(args || {});
15
+ if (input.amount <= 0) {
16
+ throw new Error('Transfer amount must be positive');
17
+ }
18
+ if (input.fromCategoryId === input.toCategoryId) {
19
+ throw new Error('Source and target categories must be different');
20
+ }
21
+ // Get current budget for both categories
22
+ const budgetMonth = await adapter.getBudgetMonth(input.month);
23
+ if (!budgetMonth || !budgetMonth.categoryGroups) {
24
+ throw new Error(`Budget not found for month ${input.month}`);
25
+ }
26
+ // Flatten categories from all groups
27
+ const allCategories = budgetMonth.categoryGroups.flatMap((group) => group.categories || []);
28
+ const fromBudget = allCategories.find((c) => c.id === input.fromCategoryId);
29
+ const toBudget = allCategories.find((c) => c.id === input.toCategoryId);
30
+ if (!fromBudget) {
31
+ throw new Error(`Source category ${input.fromCategoryId} not found in budget`);
32
+ }
33
+ if (!toBudget) {
34
+ throw new Error(`Target category ${input.toCategoryId} not found in budget`);
35
+ }
36
+ const currentFromAmount = fromBudget.budgeted || 0;
37
+ const currentToAmount = toBudget.budgeted || 0;
38
+ // Check if source has enough budget
39
+ if (currentFromAmount < input.amount) {
40
+ throw new Error(`Insufficient budget in source category. Available: ${currentFromAmount}, Requested: ${input.amount}`);
41
+ }
42
+ // Perform the transfer - use adapter.setBudgetAmount directly (already queued)
43
+ // Note: Don't use batchBudgetUpdates as it creates a nested queue deadlock
44
+ await adapter.setBudgetAmount(input.month, input.fromCategoryId, currentFromAmount - input.amount);
45
+ await adapter.setBudgetAmount(input.month, input.toCategoryId, currentToAmount + input.amount);
46
+ return {
47
+ result: {
48
+ success: true,
49
+ transferred: input.amount,
50
+ fromCategory: {
51
+ id: input.fromCategoryId,
52
+ previousAmount: currentFromAmount,
53
+ newAmount: currentFromAmount - input.amount,
54
+ },
55
+ toCategory: {
56
+ id: input.toCategoryId,
57
+ previousAmount: currentToAmount,
58
+ newAmount: currentToAmount + input.amount,
59
+ },
60
+ },
61
+ };
62
+ },
63
+ };
64
+ export default tool;
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ import { CommonSchemas } from '../lib/schemas/common.js';
4
+ const InputSchema = z.object({
5
+ name: CommonSchemas.name,
6
+ group_id: CommonSchemas.categoryGroupId,
7
+ is_income: z.boolean().nullable().optional(),
8
+ }).passthrough(); // Allow other fields to pass through
9
+ const tool = {
10
+ name: 'actual_categories_create',
11
+ description: "Create a category. REQUIRED: group_id (category group UUID). Use actual_category_groups_get to find available groups. Optional: is_income (boolean, defaults to false for expense categories).",
12
+ inputSchema: InputSchema,
13
+ call: async (args, _meta) => {
14
+ // Validate input with helpful error messages
15
+ let input;
16
+ try {
17
+ input = InputSchema.parse(args || {});
18
+ }
19
+ catch (error) {
20
+ if (error instanceof z.ZodError) {
21
+ const issues = error.issues.map(issue => {
22
+ if (issue.path.includes('group_id')) {
23
+ return `Invalid group_id: ${issue.message}. Use actual_category_groups_get to find valid category group UUIDs.`;
24
+ }
25
+ if (issue.path.includes('name')) {
26
+ return `Invalid name: ${issue.message}`;
27
+ }
28
+ if (issue.path.includes('is_income')) {
29
+ return `Invalid is_income: ${issue.message}. Must be true or false.`;
30
+ }
31
+ return `${issue.path.join('.')}: ${issue.message}`;
32
+ });
33
+ throw new Error(`Validation failed:\n${issues.join('\n')}`);
34
+ }
35
+ throw error;
36
+ }
37
+ try {
38
+ // Input already has correct field names (group_id, is_income)
39
+ // Convert null to false (LibreChat sometimes sends null instead of undefined)
40
+ const normalizedInput = {
41
+ name: input.name,
42
+ group_id: input.group_id,
43
+ is_income: input.is_income ?? false,
44
+ };
45
+ const result = await adapter.createCategory(normalizedInput);
46
+ return {
47
+ success: true,
48
+ categoryId: result,
49
+ message: `Category "${input.name}" created successfully`
50
+ };
51
+ }
52
+ catch (error) {
53
+ const errorMessage = error instanceof Error ? error.message : String(error);
54
+ // Provide helpful error messages for common issues
55
+ if (errorMessage.includes('groupId is required') || errorMessage.includes('group_id')) {
56
+ throw new Error(`Failed to create category: Invalid or missing group_id. Use actual_category_groups_get to get available category groups first.`);
57
+ }
58
+ if (errorMessage.includes('already exists') || errorMessage.includes('duplicate')) {
59
+ throw new Error(`Failed to create category: A category named "${input.name}" already exists in this group.`);
60
+ }
61
+ throw new Error(`Failed to create category: ${errorMessage}`);
62
+ }
63
+ },
64
+ };
65
+ export default tool;
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ id: z.string().describe('Category ID to delete'),
5
+ });
6
+ const tool = {
7
+ name: 'actual_categories_delete',
8
+ description: 'Delete a category from Actual Budget. Transactions using this category will need to be recategorized. This operation cannot be undone.',
9
+ inputSchema: InputSchema,
10
+ call: async (args, _meta) => {
11
+ const input = InputSchema.parse(args || {});
12
+ await adapter.deleteCategory(input.id);
13
+ return { success: true };
14
+ },
15
+ };
16
+ export default tool;
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({});
4
+ const tool = {
5
+ name: 'actual_categories_get',
6
+ description: 'List all budget categories organized by category groups. Categories are spending/income buckets (e.g., Groceries, Rent, Salary) used for budgeting and transaction categorization. Returns both grouped view and flat list with category IDs, names, and group assignments.',
7
+ inputSchema: InputSchema,
8
+ call: async (args, _meta) => {
9
+ InputSchema.parse(args || {});
10
+ const result = await adapter.getCategories();
11
+ return { result };
12
+ },
13
+ };
14
+ export default tool;
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ id: z.string().describe('Category ID to update'),
5
+ fields: z.object({
6
+ name: z.string().optional().describe('New category name'),
7
+ group_id: z.string().optional().describe('Category group ID to move this category to'),
8
+ is_income: z.boolean().optional().describe('Whether this is an income category'),
9
+ hidden: z.boolean().optional().describe('Whether this category is hidden'),
10
+ }).describe('Fields to update'),
11
+ });
12
+ const tool = {
13
+ name: 'actual_categories_update',
14
+ description: 'Update an existing category in Actual Budget. You can rename the category, move it to a different group, or change its properties.',
15
+ inputSchema: InputSchema,
16
+ call: async (args, _meta) => {
17
+ const input = InputSchema.parse(args || {});
18
+ await adapter.updateCategory(input.id, input.fields);
19
+ return { success: true };
20
+ },
21
+ };
22
+ export default tool;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ name: z.string().min(1).describe('Category group name'),
5
+ is_income: z.boolean().optional().describe('Whether this is an income category group (default: false)'),
6
+ hidden: z.boolean().optional().describe('Whether this group is hidden (default: false)'),
7
+ });
8
+ const tool = {
9
+ name: 'actual_category_groups_create',
10
+ description: `Create a new category group in Actual Budget. Category groups help organize categories into logical sections (e.g., "Fixed Expenses", "Variable Expenses", "Savings Goals"). After creating a group, you can move categories into it.`,
11
+ inputSchema: InputSchema,
12
+ call: async (args, _meta) => {
13
+ const input = InputSchema.parse(args || {});
14
+ const groupId = await adapter.createCategoryGroup(input);
15
+ return { id: groupId, success: true };
16
+ },
17
+ };
18
+ export default tool;
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ import { notFoundMsg } from '../lib/errors.js';
4
+ const InputSchema = z.object({
5
+ id: z.string().describe('Category group ID to delete'),
6
+ });
7
+ const tool = {
8
+ name: 'actual_category_groups_delete',
9
+ description: `Delete a category group from Actual Budget. Note: Categories within the group will be moved to a default group or ungrouped. This operation cannot be undone.`,
10
+ inputSchema: InputSchema,
11
+ call: async (args, _meta) => {
12
+ const input = InputSchema.parse(args || {});
13
+ // Pre-flight: verify category group exists (BUG-10)
14
+ const groups = await adapter.getCategoryGroups();
15
+ const groupExists = groups.some((g) => g.id === input.id);
16
+ if (!groupExists) {
17
+ return {
18
+ error: notFoundMsg('Category group', input.id, 'actual_category_groups_get'),
19
+ success: false,
20
+ };
21
+ }
22
+ await adapter.deleteCategoryGroup(input.id);
23
+ return { success: true };
24
+ },
25
+ };
26
+ export default tool;
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({});
4
+ const tool = {
5
+ name: 'actual_category_groups_get',
6
+ description: `List all category groups in Actual Budget. Category groups organize related categories together (e.g., "Monthly Bills" group contains "Rent", "Utilities", "Internet" categories). Each group has an ID, name, and optional properties.`,
7
+ inputSchema: InputSchema,
8
+ call: async (args, _meta) => {
9
+ const groups = await adapter.getCategoryGroups();
10
+ return { groups };
11
+ },
12
+ };
13
+ export default tool;
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ id: z.string().describe('Category group ID to update'),
5
+ fields: z.object({
6
+ name: z.string().optional().describe('New group name'),
7
+ is_income: z.boolean().optional().describe('Whether this is an income category group'),
8
+ hidden: z.boolean().optional().describe('Whether this group is hidden'),
9
+ }).describe('Fields to update'),
10
+ });
11
+ const tool = {
12
+ name: 'actual_category_groups_update',
13
+ description: `Update an existing category group in Actual Budget. You can rename the group, change whether it's an income group, or hide/show it.`,
14
+ inputSchema: InputSchema,
15
+ call: async (args, _meta) => {
16
+ const input = InputSchema.parse(args || {});
17
+ await adapter.updateCategoryGroup(input.id, input.fields);
18
+ return { success: true };
19
+ },
20
+ };
21
+ export default tool;
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const ALLOWED_TYPES = ['accounts', 'schedules', 'categories', 'payees'];
4
+ const InputSchema = z.object({
5
+ type: z
6
+ .enum(ALLOWED_TYPES)
7
+ .describe("Entity type to look up. One of: 'accounts', 'schedules', 'categories', 'payees'"),
8
+ name: z
9
+ .string()
10
+ .min(1, 'Name cannot be empty')
11
+ .describe('Exact name of the entity to resolve to an ID'),
12
+ });
13
+ const tool = {
14
+ name: 'actual_get_id_by_name',
15
+ description: `Resolve an entity name to its UUID.
16
+
17
+ Looks up the UUID for any Account, Payee, Category, or Schedule by their display name.
18
+ This is useful when you know the human-readable name but need the ID for other API calls.
19
+
20
+ Allowed types: 'accounts', 'schedules', 'categories', 'payees'
21
+
22
+ Returns the UUID string for the matching entity.
23
+
24
+ Examples:
25
+ - Find the ID for an account named "Checking Account"
26
+ - Find the ID for a category named "Groceries"
27
+ - Find the ID for a payee named "Amazon"
28
+ - Find the ID for a schedule named "Rent"`,
29
+ inputSchema: InputSchema,
30
+ call: async (args) => {
31
+ const input = InputSchema.parse(args);
32
+ const id = await adapter.getIDByName(input.type, input.name);
33
+ return { id, type: input.type, name: input.name };
34
+ },
35
+ };
36
+ export default tool;
@@ -0,0 +1,63 @@
1
+ // Auto-generated index for all tool modules
2
+ export { default as accounts_close } from './accounts_close.js';
3
+ export { default as accounts_create } from './accounts_create.js';
4
+ export { default as accounts_delete } from './accounts_delete.js';
5
+ export { default as accounts_get_balance } from './accounts_get_balance.js';
6
+ export { default as accounts_list } from './accounts_list.js';
7
+ export { default as accounts_reopen } from './accounts_reopen.js';
8
+ export { default as accounts_update } from './accounts_update.js';
9
+ export { default as bank_sync } from './bank_sync.js';
10
+ export { default as budget_updates_batch } from './budget_updates_batch.js';
11
+ export { default as budgets_get_all } from './budgets_get_all.js';
12
+ export { default as budgets_list_available } from './budgets_list_available.js';
13
+ export { default as budgets_switch } from './budgets_switch.js';
14
+ export { default as budgets_getMonth } from './budgets_getMonth.js';
15
+ export { default as budgets_getMonths } from './budgets_getMonths.js';
16
+ export { default as budgets_holdForNextMonth } from './budgets_holdForNextMonth.js';
17
+ export { default as budgets_resetHold } from './budgets_resetHold.js';
18
+ export { default as budgets_setAmount } from './budgets_setAmount.js';
19
+ export { default as budgets_setCarryover } from './budgets_setCarryover.js';
20
+ export { default as budgets_transfer } from './budgets_transfer.js';
21
+ export { default as categories_create } from './categories_create.js';
22
+ export { default as categories_delete } from './categories_delete.js';
23
+ export { default as categories_get } from './categories_get.js';
24
+ export { default as categories_update } from './categories_update.js';
25
+ export { default as category_groups_create } from './category_groups_create.js';
26
+ export { default as category_groups_delete } from './category_groups_delete.js';
27
+ export { default as category_groups_get } from './category_groups_get.js';
28
+ export { default as category_groups_update } from './category_groups_update.js';
29
+ export { default as payee_rules_get } from './payee_rules_get.js';
30
+ export { default as payees_create } from './payees_create.js';
31
+ export { default as payees_delete } from './payees_delete.js';
32
+ export { default as payees_get } from './payees_get.js';
33
+ export { default as payees_merge } from './payees_merge.js';
34
+ export { default as payees_update } from './payees_update.js';
35
+ export { default as get_id_by_name } from './get_id_by_name.js';
36
+ export { default as server_get_version } from './server_get_version.js';
37
+ export { default as query_run } from './query_run.js';
38
+ export { default as rules_create } from './rules_create.js';
39
+ export { default as rules_create_or_update } from './rules_create_or_update.js';
40
+ export { default as rules_delete } from './rules_delete.js';
41
+ export { default as rules_get } from './rules_get.js';
42
+ export { default as rules_update } from './rules_update.js';
43
+ export { default as schedules_create } from './schedules_create.js';
44
+ export { default as schedules_delete } from './schedules_delete.js';
45
+ export { default as schedules_get } from './schedules_get.js';
46
+ export { default as schedules_update } from './schedules_update.js';
47
+ export { default as server_info } from './server_info.js';
48
+ export { default as transactions_create } from './transactions_create.js';
49
+ export { default as transactions_delete } from './transactions_delete.js';
50
+ export { default as transactions_filter } from './transactions_filter.js';
51
+ export { default as transactions_get } from './transactions_get.js';
52
+ export { default as transactions_import } from './transactions_import.js';
53
+ export { default as transactions_search_by_amount } from './transactions_search_by_amount.js';
54
+ export { default as transactions_search_by_category } from './transactions_search_by_category.js';
55
+ export { default as transactions_search_by_month } from './transactions_search_by_month.js';
56
+ export { default as transactions_search_by_payee } from './transactions_search_by_payee.js';
57
+ export { default as transactions_summary_by_category } from './transactions_summary_by_category.js';
58
+ export { default as transactions_summary_by_payee } from './transactions_summary_by_payee.js';
59
+ export { default as transactions_uncategorized } from './transactions_uncategorized.js';
60
+ export { default as transactions_update } from './transactions_update.js';
61
+ export { default as transactions_update_batch } from './transactions_update_batch.js';
62
+ export { default as session_close } from './session_close.js';
63
+ export { default as session_list } from './session_list.js';
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ import { notFoundMsg } from '../lib/errors.js';
4
+ const InputSchema = z.object({
5
+ payeeId: z.string().describe('ID of the payee to get rules for'),
6
+ });
7
+ const tool = {
8
+ name: 'actual_payee_rules_get',
9
+ description: `Get all payee rules associated with a specific payee. Returns PayeeRule objects that show how transactions with this payee are automatically processed (conditions and actions).`,
10
+ inputSchema: InputSchema,
11
+ call: async (args, _meta) => {
12
+ const input = InputSchema.parse(args || {});
13
+ // Pre-flight: verify payee exists and get its name (BUG-3)
14
+ const payees = await adapter.getPayees();
15
+ const payee = payees.find((p) => p.id === input.payeeId);
16
+ if (!payee) {
17
+ return {
18
+ error: notFoundMsg('Payee', input.payeeId, 'actual_payees_get'),
19
+ rules: [],
20
+ count: 0,
21
+ };
22
+ }
23
+ const rules = await adapter.getPayeeRules(input.payeeId);
24
+ return { rules, count: rules.length, payeeName: payee.name };
25
+ },
26
+ };
27
+ export default tool;
@@ -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
+ export default createTool({
6
+ name: 'actual_payees_create',
7
+ description: 'Create a new payee in Actual Budget',
8
+ schema: z.object({
9
+ name: CommonSchemas.name,
10
+ notes: CommonSchemas.notes
11
+ }),
12
+ handler: async (input) => {
13
+ return await adapter.createPayee(input);
14
+ },
15
+ examples: [
16
+ {
17
+ description: 'Create a payee for a grocery store',
18
+ input: { name: 'Whole Foods' },
19
+ },
20
+ {
21
+ description: 'Create a payee with notes',
22
+ input: { name: 'Electric Company', notes: 'Monthly utility payment' },
23
+ },
24
+ ],
25
+ });
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ id: z.string().describe('Payee ID to delete'),
5
+ });
6
+ const tool = {
7
+ name: 'actual_payees_delete',
8
+ description: 'Delete a payee from Actual Budget. Transactions using this payee will have it removed. This operation cannot be undone.',
9
+ inputSchema: InputSchema,
10
+ call: async (args, _meta) => {
11
+ const input = InputSchema.parse(args || {});
12
+ await adapter.deletePayee(input.id);
13
+ return { success: true };
14
+ },
15
+ };
16
+ export default tool;
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({});
4
+ const tool = {
5
+ name: 'actual_payees_get',
6
+ description: "List all payees in Actual Budget. Payees represent merchants, service providers, individuals, or other entities you transact with. Returns payee ID, name, and optional transfer account information for internal transfers.",
7
+ inputSchema: InputSchema,
8
+ call: async (args, _meta) => {
9
+ InputSchema.parse(args || {});
10
+ const result = await adapter.getPayees();
11
+ return { result };
12
+ },
13
+ };
14
+ export default tool;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import adapter from '../lib/actual-adapter.js';
3
+ const InputSchema = z.object({
4
+ targetId: z.string().describe('ID of the target payee to merge into (this payee will be retained)'),
5
+ mergeIds: z.array(z.string()).describe('Array of payee IDs to merge into the target payee (these will be consolidated)'),
6
+ });
7
+ const tool = {
8
+ name: 'actual_payees_merge',
9
+ description: `Merge one or more payees into a target payee. This consolidates duplicate payees by merging the specified payees into the target, retaining the name of the target payee. All transactions from merged payees will be reassigned to the target payee.`,
10
+ inputSchema: InputSchema,
11
+ call: async (args, _meta) => {
12
+ const input = InputSchema.parse(args || {});
13
+ await adapter.mergePayees(input.targetId, input.mergeIds);
14
+ return { success: true, message: `Successfully merged ${input.mergeIds.length} payee(s) into target payee` };
15
+ },
16
+ };
17
+ export default tool;
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import { CommonSchemas } from '../lib/schemas/common.js';
3
+ import adapter from '../lib/actual-adapter.js';
4
+ const InputSchema = z.object({
5
+ id: CommonSchemas.payeeId,
6
+ fields: z.object({
7
+ name: CommonSchemas.name.optional(),
8
+ transfer_acct: CommonSchemas.accountId.optional().describe('Transfer account if payee represents account transfer'),
9
+ category: CommonSchemas.categoryId.optional().nullable().describe('Default category ID for this payee (null to clear)'),
10
+ }).strict().optional().describe('Fields to update - only recognized fields allowed'),
11
+ });
12
+ const tool = {
13
+ name: 'actual_payees_update',
14
+ description: `Update an existing payee in Actual Budget.
15
+
16
+ Updatable fields:
17
+ - name: Payee name (1-255 chars)
18
+ - transfer_acct: Transfer account ID if this payee represents an account transfer (optional)
19
+ - category: Default category ID to auto-assign to transactions from this payee (optional, null to clear)
20
+
21
+ Example: Update payee name:
22
+ {
23
+ "id": "<payee-uuid>",
24
+ "fields": {
25
+ "name": "Grocery Store"
26
+ }
27
+ }
28
+
29
+ Example: Mark payee as transfer account:
30
+ {
31
+ "id": "<payee-uuid>",
32
+ "fields": {
33
+ "transfer_acct": "<account-uuid>"
34
+ }
35
+ }`,
36
+ inputSchema: InputSchema,
37
+ call: async (args, _meta) => {
38
+ try {
39
+ const input = InputSchema.parse(args || {});
40
+ if (!input.fields || Object.keys(input.fields).length === 0) {
41
+ throw new Error('No fields provided to update. Include at least one field: name or transfer_acct.');
42
+ }
43
+ await adapter.updatePayee(input.id, input.fields);
44
+ return {
45
+ success: true,
46
+ payeeId: input.id,
47
+ updatedFields: Object.keys(input.fields),
48
+ };
49
+ }
50
+ catch (error) {
51
+ if (error instanceof z.ZodError) {
52
+ const fieldErrors = error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ');
53
+ throw new Error(`Invalid payee update data: ${fieldErrors}`);
54
+ }
55
+ throw error;
56
+ }
57
+ },
58
+ };
59
+ export default tool;