actual-mcp-server 0.25.1 → 0.26.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
@@ -15,7 +15,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server that lets an
15
15
 
16
16
  **What you can ask it:** "How much did I spend on groceries last month, by payee?", "Add a $47.50 grocery transaction at Whole Foods to my checking account.", "Move $200 from Dining Out to Emergency Fund for this month."
17
17
 
18
- **84 tools** across accounts, transactions, budgets, rules, schedules and more, over stdio or HTTP, with Bearer token or OIDC auth, per-user budget access, up to 15 concurrent sessions, and automatic retry of transient Actual errors (3 attempts, exponential backoff; deletes and merges are never retried).
18
+ **85 tools** across accounts, transactions, budgets, rules, schedules and more, over stdio or HTTP, with Bearer token or OIDC auth, per-user budget access, up to 15 concurrent sessions, and automatic retry of transient Actual errors (3 attempts, exponential backoff; deletes and merges are never retried).
19
19
 
20
20
  ## Pick your path
21
21
 
@@ -98,7 +98,7 @@ Every tool, with its description and caveats, is in the [tools reference](docs/r
98
98
 
99
99
  ### Reducing the tool surface
100
100
 
101
- All 84 tools are published by default. Chat clients with a small tool budget (Gemini, LibreChat) do better with fewer. A tool that is not published is also refused if a client calls it by name.
101
+ All 85 tools are published by default. Chat clients with a small tool budget (Gemini, LibreChat) do better with fewer. A tool that is not published is also refused if a client calls it by name.
102
102
 
103
103
  <!-- BEGIN GENERATED: tools-summary -->
104
104
  <!-- Generated from dist/src/lib/toolsets.js by `npm run docs:gen`. Do not edit. -->
@@ -109,14 +109,14 @@ All 84 tools are published by default. Chat clients with a small tool budget (Ge
109
109
  | `analysis` | 7 | Summaries, aggregates and balances |
110
110
  | `budget` | 10 | Budget months, amounts, carryover, holds, transfers, batch updates and notes |
111
111
  | `rules` | 7 | Rules and payee rules, including `actual_rules_create_batch` |
112
- | `schedules` | 4 | Scheduled transactions |
112
+ | `schedules` | 5 | Scheduled transactions |
113
113
  | `structure` | 21 | Accounts, account groups, categories, category groups, payees and tags |
114
114
  | `query` | 1 | `actual_query_run` (SELECT only) |
115
115
  | `admin` | 6 | Budget switching, import, export, bank sync and session management |
116
116
 
117
117
  The `chat` preset publishes 12 tools: `actual_get_context`, `actual_query_run`, `actual_transactions_update_batch`, `actual_budget_updates_batch`, `actual_rules_create_batch`, `actual_transactions_create`, `actual_transactions_update`, `actual_transactions_delete`, `actual_rules_create`, `actual_budgets_transfer`, `actual_budgets_setAmount` and `actual_budgets_getMonth`.
118
118
 
119
- `MCP_READ_ONLY=true` hides and refuses 48 write-capable tools and leaves 36 published.
119
+ `MCP_READ_ONLY=true` hides and refuses 48 write-capable tools and leaves 37 published.
120
120
  <!-- END GENERATED: tools-summary -->
121
121
 
122
122
  Example: a chat assistant with two extra tools.
@@ -169,4 +169,4 @@ MIT, see [LICENSE](LICENSE). Built on [Actual Budget](https://actualbudget.org/)
169
169
 
170
170
  ---
171
171
 
172
- **Version:** 0.25.1 | **Tool Count:** 84 (verified LibreChat-compatible)
172
+ **Version:** 0.26.0 | **Tool Count:** 85 (verified LibreChat-compatible)
@@ -57,6 +57,7 @@ const IMPLEMENTED_TOOLS = [
57
57
  'actual_rules_get',
58
58
  'actual_rules_update',
59
59
  'actual_schedules_create',
60
+ 'actual_schedules_dates',
60
61
  'actual_schedules_delete',
61
62
  'actual_schedules_get',
62
63
  'actual_schedules_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, mergeTransactions: rawMergeTransactions, getPayeeRules: rawGetPayeeRules, 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,
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, mergeTransactions: rawMergeTransactions, getPayeeRules: rawGetPayeeRules, holdBudgetForNextMonth: rawHoldBudgetForNextMonth, resetBudgetHold: rawResetBudgetHold, runQuery: rawRunQuery, runBankSync: rawRunBankSync, getBudgets: rawGetBudgets, getIDByName: rawGetIDByName, getServerVersion: rawGetServerVersion, getSchedules: rawGetSchedules, getScheduleDates: rawGetScheduleDates, 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';
@@ -13,7 +13,7 @@ import observability from '../observability.js';
13
13
  import { retry, isRetryableError, isRateLimitError } from './retry.js';
14
14
  import { withOpTimeout } from './opTimeout.js';
15
15
  import { markInitFailure } from './init-failure.js';
16
- import { NotFoundRefusal, OutOfRangeRefusal, constraintErrorMsg, errorLogFields } from './errors.js';
16
+ import { NotFoundRefusal, OutOfRangeRefusal, constraintErrorMsg, errorLogFields, isPreflightRefusal } from './errors.js';
17
17
  import { amountOpMismatch } from './schemas/schedule-amount.js';
18
18
  import { findMatchingRule } from './rule-matching.js';
19
19
  import logger from '../logger.js';
@@ -925,7 +925,7 @@ async function shutdownActualApi(opts) {
925
925
  setApiInitialized(false);
926
926
  }
927
927
  }
928
- import { BANK_SYNC_SETTLE_MS, SPLIT_VERIFY_INTERVAL_MS, SPLIT_VERIFY_TIMEOUT_MS, WRITE_SESSION_DELAY_MS } from './constants.js';
928
+ import { SCHEDULE_DATES_CAP, SPLIT_VERIFY_INTERVAL_MS, SPLIT_VERIFY_TIMEOUT_MS, WRITE_SESSION_DELAY_MS } from './constants.js';
929
929
  // Concurrency limiter extracted to ./actual-adapter/concurrency.ts (#166).
930
930
  // Imported for internal use (every method wraps its raw call in withConcurrency)
931
931
  // and setMaxConcurrency is re-exported.
@@ -2958,6 +2958,56 @@ export async function getSchedules() {
2958
2958
  return Array.isArray(raw) ? raw : [];
2959
2959
  });
2960
2960
  }
2961
+ /**
2962
+ * #479: the dates a schedule falls on, expanded by Actual itself (`api.getScheduleDates`, new in
2963
+ * 26.11.0), so month ends, patterns, intervals and the weekend move match the Actual UI.
2964
+ *
2965
+ * One session: the schedule listing and the expansion run inside the same withActualApi, never
2966
+ * through adapter.getSchedules (the api lock is not reentrant). Upstream takes the schedule row's
2967
+ * `date` field (`scheduleModel.toExternal` maps it from `_date`), a single date or a RecurConfig.
2968
+ * A count is ALWAYS sent, one above what is returned, so an open-ended recurrence returns its next
2969
+ * dates instead of upstream's "needs an end or a count" error, and the extra date is how
2970
+ * `truncated` knows that more dates exist after the last one returned. With both `end` and
2971
+ * `count`, upstream stops at whichever comes first.
2972
+ *
2973
+ * The two refusals a caller can fix are typed and checked BEFORE the upstream call, never chosen
2974
+ * by matching upstream message text: an `end` before `start` (only when both are given; with
2975
+ * `start` omitted upstream uses its own today and its error passes through), and a stored
2976
+ * `skipWeekend` without `weekendSolveMode`. Any other upstream error passes through unchanged.
2977
+ */
2978
+ export async function getScheduleDates(scheduleId, opts = {}) {
2979
+ if (opts.start !== undefined && opts.end !== undefined && opts.end < opts.start) {
2980
+ throw new OutOfRangeRefusal(`end (${opts.end}) is before start (${opts.start}); end must be on or after start.`, opts.end);
2981
+ }
2982
+ return withActualApi(async () => {
2983
+ observability.incrementToolCall('actual.schedules.dates').catch(() => { });
2984
+ const schedules = await withConcurrency(() => retry(() => rawGetSchedules(), { retries: 2, backoffMs: 200 }));
2985
+ const schedule = Array.isArray(schedules) ? schedules.find((s) => s?.id === scheduleId) : undefined;
2986
+ if (!schedule) {
2987
+ throw new NotFoundRefusal('Schedule', scheduleId, 'actual_schedules_get');
2988
+ }
2989
+ const recur = schedule.date;
2990
+ if (recur && typeof recur === 'object' && recur.skipWeekend && !recur.weekendSolveMode) {
2991
+ throw new OutOfRangeRefusal(`Schedule ${scheduleId} has skipWeekend set without weekendSolveMode, so its dates cannot be ` +
2992
+ "computed. Set weekendSolveMode to 'before' or 'after' with actual_schedules_update.", 'weekendSolveMode');
2993
+ }
2994
+ const limit = Math.min(opts.count ?? SCHEDULE_DATES_CAP, SCHEDULE_DATES_CAP);
2995
+ const raw = await withConcurrency(() => rawGetScheduleDates(schedule.date, {
2996
+ ...(opts.start !== undefined ? { start: opts.start } : {}),
2997
+ ...(opts.end !== undefined ? { end: opts.end } : {}),
2998
+ count: limit + 1,
2999
+ }));
3000
+ const all = Array.isArray(raw) ? raw : [];
3001
+ const dates = all.slice(0, limit);
3002
+ return {
3003
+ schedule_id: scheduleId,
3004
+ name: typeof schedule.name === 'string' ? schedule.name : null,
3005
+ dates,
3006
+ count: dates.length,
3007
+ truncated: all.length > limit,
3008
+ };
3009
+ });
3010
+ }
2961
3011
  /**
2962
3012
  * #547: upstream builds a schedule's rule with one `is` condition per field and STRINGIFIES the
2963
3013
  * value on create (`scheduleModel.fromExternal` in loot-core/src/server/api-models.ts), so an
@@ -3863,75 +3913,73 @@ export async function getFinancialAnalysisSnapshot(params) {
3863
3913
  // Imported for internal use by runQuery and re-exported (unit-tested directly).
3864
3914
  import { parseWhereClause, nameConditionLookups, nameConditionHintLines } from './actual-adapter/query.js';
3865
3915
  export { parseWhereClause };
3916
+ function normaliseBankSyncReport(raw) {
3917
+ const r = (raw && typeof raw === 'object' ? raw : {});
3918
+ const list = (v) => (Array.isArray(v) ? v.map(String) : []);
3919
+ return {
3920
+ newTransactions: list(r.newTransactions),
3921
+ matchedTransactions: list(r.matchedTransactions),
3922
+ updatedAccounts: list(r.updatedAccounts),
3923
+ };
3924
+ }
3925
+ const BANK_SYNC_SETUP_HINT = 'To use bank sync, link your account(s) with a supported provider (GoCardless or SimpleFIN) in the Actual Budget UI. ' +
3926
+ 'See https://actualbudget.org/docs/advanced/bank-sync for setup instructions.';
3866
3927
  export async function runBankSync(accountId) {
3867
3928
  try {
3868
- return await withActualApi(async () => {
3929
+ // #548: a write, so the import and the bank_sync_status row reach the server. In pooled
3930
+ // mode withActualApiWrite pushes once after a RESOLVED sync and never in a rejection path;
3931
+ // on the fallback (init/op/shutdown) path, shutdown still syncs whatever a partial
3932
+ // all-accounts run imported before it failed. Not queueWriteOperation: bank sync is one
3933
+ // upstream call, and the queue's per-call bound has no env knob, which #619 owns.
3934
+ return await withActualApiWrite(async () => {
3869
3935
  observability.incrementToolCall('actual.bank.sync').catch(() => { });
3870
- // Bank sync must NOT be retried — retrying could import duplicate transactions.
3936
+ // Bank sync must NOT be retried: retrying could import duplicate transactions.
3871
3937
  // Pass { accountId } for a specific account, or {} to sync all linked accounts.
3872
3938
  const args = accountId != null ? { accountId } : {};
3873
- // Pre-check: verify bank-linked accounts exist before calling rawRunBankSync.
3874
- // The SDK silently resolves void for local accounts (account_sync_source: null),
3875
- // which would otherwise be misreported as success and cause an unnecessary 30s wait.
3939
+ // Pre-check: upstream resolves silently for a local account and skips a closed one,
3940
+ // which would read as an empty but successful sync. Refusals are typed (#377).
3876
3941
  if (accountId != null) {
3877
- // Per-account check: verify the specified account is bank-linked.
3878
3942
  const { data: acctRows } = await rawRunQuery(api.q('accounts')
3879
- .select(['account_sync_source', 'name'])
3943
+ .select(['account_sync_source', 'account_id', 'name', 'closed'])
3880
3944
  .filter({ id: accountId, tombstone: false }));
3881
3945
  const acct = acctRows?.[0];
3882
3946
  if (!acct) {
3883
- throw new Error(`Bank sync failed: Account not found (id: ${accountId})`);
3947
+ // Only reachable on a race: the tool resolves the id with verifyExists first (#388).
3948
+ throw new NotFoundRefusal('Account', accountId, 'actual_accounts_list');
3884
3949
  }
3885
3950
  if (!acct.account_sync_source) {
3886
- throw new Error(`Bank sync failed: Account "${acct.name}" is a local account — not configured for bank sync. ` +
3887
- `To use bank sync, link your account with a supported provider (GoCardless or SimpleFIN) in the Actual Budget UI. ` +
3888
- `See https://actualbudget.org/docs/advanced/bank-sync for setup instructions.`);
3951
+ throw new OutOfRangeRefusal(`Bank sync failed: Account "${acct.name}" is a local account, not configured for bank sync. ${BANK_SYNC_SETUP_HINT}`, accountId);
3952
+ }
3953
+ if (acct.closed) {
3954
+ throw new OutOfRangeRefusal(`Bank sync failed: Account "${acct.name}" is closed; Actual skips closed accounts in bank sync. Reopen it in Actual to sync it.`, accountId);
3955
+ }
3956
+ // Upstream also skips an account without a provider account id (and one without a bank
3957
+ // row, which AQL does not expose, so that rarer half-link still reads as an empty sync).
3958
+ if (!acct.account_id) {
3959
+ throw new OutOfRangeRefusal(`Bank sync failed: Account "${acct.name}" has a ${acct.account_sync_source} link with no provider account; Actual skips it in bank sync. Re-link it in the Actual Budget UI.`, accountId);
3889
3960
  }
3890
3961
  }
3891
3962
  else {
3892
- // Global check: verify at least one bank-linked account exists across the budget.
3893
3963
  const { data: allAccounts } = await rawRunQuery(api.q('accounts')
3894
- .select(['account_sync_source'])
3964
+ .select(['account_sync_source', 'account_id', 'closed'])
3895
3965
  .filter({ tombstone: false }));
3896
- const linkedCount = allAccounts?.filter(a => a.account_sync_source).length ?? 0;
3897
- if (linkedCount === 0) {
3898
- throw new Error(`Bank sync failed: No accounts are configured for bank sync. ` +
3899
- `To use bank sync, link your account(s) with a supported provider (GoCardless or SimpleFIN) in the Actual Budget UI. ` +
3900
- `See https://actualbudget.org/docs/advanced/bank-sync for setup instructions.`);
3901
- }
3902
- }
3903
- // rawRunBankSync returns void immediately; the actual provider call runs on
3904
- // a background promise inside the SDK and surfaces errors as unhandledRejection.
3905
- // We install a temporary listener to capture any BankSyncError and re-throw.
3906
- let capturedRejection = null;
3907
- const rejectionHandler = (reason) => {
3908
- const msg = reason?.message || String(reason);
3909
- if (reason?.type === 'BankSyncError' ||
3910
- msg.includes('BankSyncError') ||
3911
- msg.includes('NORDIGEN_ERROR') ||
3912
- msg.includes('Rate limit exceeded') ||
3913
- msg.includes('Failed syncing account') ||
3914
- msg.includes('GoCardless') ||
3915
- msg.includes('SimpleFIN')) {
3916
- capturedRejection = reason;
3966
+ const linked = (allAccounts ?? []).filter(a => a.account_sync_source && a.account_id);
3967
+ if (!linked.some(a => !a.closed)) {
3968
+ throw new OutOfRangeRefusal(linked.length > 0
3969
+ ? 'Bank sync failed: Every bank-linked account is closed; Actual skips closed accounts in bank sync. Reopen one in Actual to sync it.'
3970
+ : `Bank sync failed: No accounts are configured for bank sync. ${BANK_SYNC_SETUP_HINT}`, 'all');
3917
3971
  }
3918
- };
3919
- process.on('unhandledRejection', rejectionHandler);
3920
- try {
3921
- await rawRunBankSync(args);
3922
- // Wait for the SDK's background promise to resolve/reject.
3923
- // Provider errors (rate limits, auth failures) arrive in < 3s in practice;
3924
- // BANK_SYNC_SETTLE_MS gives a comfortable margin to catch them.
3925
- await new Promise(resolve => setTimeout(resolve, BANK_SYNC_SETTLE_MS));
3926
- if (capturedRejection !== null)
3927
- throw capturedRejection;
3928
- }
3929
- finally {
3930
- process.off('unhandledRejection', rejectionHandler);
3931
3972
  }
3973
+ // Since 26.11 upstream awaits the provider and rejects this call itself, so the old
3974
+ // settle sleep and the process-wide unhandledRejection listener are gone (#548).
3975
+ return normaliseBankSyncReport(await rawRunBankSync(args));
3932
3976
  });
3933
3977
  }
3934
3978
  catch (error) {
3979
+ // A typed refusal is already the caller-facing answer; the text mapping below would
3980
+ // rewrite "not configured" into a generic message and lose the brand.
3981
+ if (isPreflightRefusal(error))
3982
+ throw error;
3935
3983
  const errorMsg = error?.message || String(error);
3936
3984
  // Network / connectivity errors (includes "fetch failed" from Node.js native fetch)
3937
3985
  if (errorMsg.includes('fetch failed') ||
@@ -3952,22 +4000,16 @@ export async function runBankSync(accountId) {
3952
4000
  `To use bank sync, you must first link your account(s) with a supported provider (GoCardless or SimpleFIN) in the Actual Budget UI. ` +
3953
4001
  `See https://actualbudget.org/docs/advanced/bank-sync for setup instructions.`);
3954
4002
  }
3955
- // GoCardless / SimpleFIN provider-level errors
3956
- // BankSyncError objects (from @actual-app/api) may have { type, category, code, message }
3957
- const bankSyncCategory = error?.category || '';
3958
- if (errorMsg.includes('Rate limit exceeded') ||
3959
- errorMsg.includes('RATE_LIMIT_EXCEEDED') ||
3960
- bankSyncCategory === 'RATE_LIMIT_EXCEEDED') {
3961
- const reset = error?.details?.rateLimitHeaders?.http_x_ratelimit_account_success_reset;
3962
- const retryIn = reset ? ` Retry in ~${Math.ceil(Number(reset) / 60)} minute(s).` : '';
3963
- throw new Error(`Bank sync failed: GoCardless rate limit exceeded for this account.${retryIn} ` +
4003
+ // Provider errors. Since 26.11 the rejection is a plain Error with `.code` only (no
4004
+ // category, type or details), so the upstream message is the one rate-limit signal.
4005
+ if (errorMsg.includes('Rate limit exceeded')) {
4006
+ throw new Error(`Bank sync failed: GoCardless rate limit exceeded for this account. ` +
3964
4007
  `(NORDIGEN RATE_LIMIT_EXCEEDED: account success quota exhausted)`);
3965
4008
  }
3966
- if (error?.type === 'BankSyncError' ||
3967
- errorMsg.includes('BankSyncError') ||
3968
- errorMsg.includes('NORDIGEN_ERROR') ||
3969
- errorMsg.includes('Failed syncing account')) {
3970
- throw new Error(`Bank sync failed: Provider error: ${errorMsg}`, { cause: error });
4009
+ if (errorMsg.includes('Failed syncing account')) {
4010
+ // The message names only the account; `.code` is the one hint at why (re-link or retry).
4011
+ const code = typeof error?.code === 'string' && error.code ? ` (code ${error.code})` : '';
4012
+ throw new Error(`Bank sync failed: Provider error: ${errorMsg}${code}`, { cause: error });
3971
4013
  }
3972
4014
  throw new Error(`Bank sync failed: ${errorMsg}`, { cause: error }); // #452: keep the brand
3973
4015
  }
@@ -4573,6 +4615,7 @@ export default {
4573
4615
  getIDByName,
4574
4616
  getServerVersion,
4575
4617
  getSchedules,
4618
+ getScheduleDates,
4576
4619
  createSchedule,
4577
4620
  updateSchedule,
4578
4621
  deleteSchedule,
@@ -31,15 +31,11 @@ export const DEFAULT_CONCURRENCY_LIMIT = 5;
31
31
  // TIMEOUTS
32
32
  // ============================================================================
33
33
  /**
34
- * How long (ms) to wait after calling rawRunBankSync for the SDK's background
35
- * promise to surface a BankSyncError as an unhandledRejection.
36
- *
37
- * Bank provider errors (GoCardless RATE_LIMIT_EXCEEDED, auth failures, etc.)
38
- * arrive as HTTP responses. Fast banks respond within 1-3 seconds; slower
39
- * institutions can take considerably longer. 30 seconds gives a comfortable
40
- * margin while keeping the tool's wall-clock time acceptable for MCP clients.
34
+ * How many ids each list of an `actual_bank_sync` report carries (#548). The counts are
35
+ * always the full lengths; only the id lists are cut, so a first sync of a large account
36
+ * cannot flood the client's context. The tool sets `truncated` when any list was cut.
41
37
  */
42
- export const BANK_SYNC_SETTLE_MS = 30_000;
38
+ export const BANK_SYNC_IDS_CAP = 100;
43
39
  /**
44
40
  * How long (ms) to wait for additional queued writes before closing the
45
41
  * shared budget session. Increasing this value batches more writes per
@@ -80,6 +76,10 @@ export const DEFAULT_HTTP_PORT = 3600;
80
76
  // ============================================================================
81
77
  // VALIDATION & LIMITS
82
78
  // ============================================================================
79
+ /**
80
+ * #479: the most dates actual_schedules_dates returns, so a daily schedule cannot flood the context.
81
+ */
82
+ export const SCHEDULE_DATES_CAP = 366;
83
83
  /**
84
84
  * Maximum length for name fields (accounts, categories, payees)
85
85
  */
@@ -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 84 tool definitions.
6
+ * and reduce duplication across the 85 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';
@@ -44,13 +44,13 @@ const OPEN_WORLD = new Set(['actual_bank_sync']);
44
44
  * `tests/unit/tool_annotations.test.js`, which resolves each tool's `adapter.*` calls and
45
45
  * fails if one of these reaches `queueWriteOperation`.
46
46
  *
47
- * Claimed only where it is certain. Three read-path tools are deliberately absent because
47
+ * Claimed only where it is certain. Three tools are deliberately absent because
48
48
  * "does not modify its environment" is false for them, and a wrong `true` here is worse
49
49
  * than no annotation at all:
50
50
  *
51
- * actual_bank_sync runs through `withActualApi` (the read path) but IMPORTS
52
- * transactions, so the call graph says read and reality says
53
- * write. Reality wins.
51
+ * actual_bank_sync IMPORTS transactions through `withActualApiWrite` (one
52
+ * upstream call, not the write queue, #548), so a queue-based
53
+ * reading of the call graph misses it.
54
54
  * actual_budgets_export writes a zip into the server's data directory.
55
55
  * actual_budgets_switch changes the session's active budget and persists a preference.
56
56
  * actual_session_close closes a pooled connection: server state, not budget data.
@@ -77,6 +77,7 @@ const READ_ONLY = new Set([
77
77
  'actual_preferences_get',
78
78
  'actual_query_run',
79
79
  'actual_rules_get',
80
+ 'actual_schedules_dates',
80
81
  'actual_schedules_get',
81
82
  'actual_server_get_version',
82
83
  'actual_server_info',
@@ -94,6 +94,7 @@ export const TOOLSETS = {
94
94
  ],
95
95
  schedules: [
96
96
  'actual_schedules_get',
97
+ 'actual_schedules_dates',
97
98
  'actual_schedules_create',
98
99
  'actual_schedules_update',
99
100
  'actual_schedules_delete',
@@ -170,7 +171,7 @@ export const PRESET_NAMES = Object.keys(PRESETS);
170
171
  *
171
172
  * Derived by hand from the adapter call graph: the 43 tools whose adapter path reaches
172
173
  * queueWriteOperation, plus the four that mutate WITHOUT the write queue (bank_sync imports
173
- * transactions through the read path, budgets_export writes a zip to ACTUAL_EXPORT_DIR,
174
+ * transactions through withActualApiWrite, budgets_export writes a zip to ACTUAL_EXPORT_DIR,
174
175
  * budgets_switch changes the active budget, session_close closes a pooled connection).
175
176
  * Never derive this from tool annotations.
176
177
  *
@@ -1,11 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
+ import { BANK_SYNC_IDS_CAP } from '../lib/constants.js';
3
4
  const InputSchema = z.object({
4
5
  accountId: z.string().nullable().optional().describe('Optional account ID to sync a specific account. If omitted, syncs all linked accounts.'),
5
6
  });
6
7
  const tool = {
7
8
  name: 'actual_bank_sync',
8
- description: 'Trigger 3rd party bank sync (GoCardless, SimpleFIN) for linked bank accounts. Returns immediately with an error if no bank-linked accounts are found. When bank-linked accounts exist, waits up to 30 seconds for the provider to confirm the operation and surfaces errors such as rate limits or auth failures that occur within that window. Successful syncs may take a few additional moments for transactions to appear in Actual Budget.',
9
+ description: 'Run 3rd party bank sync (GoCardless, SimpleFIN) for one linked account, or for every linked open account when accountId is omitted. ' +
10
+ 'Refuses at once for a local, closed, half-linked or unknown account, or when no open account is linked. ' +
11
+ 'Reports what Actual reconciled: newTransactions (imported), matchedTransactions (existing ones updated) and updatedAccounts (accounts that received new transactions), ' +
12
+ `each as { count, ids } with up to ${BANK_SYNC_IDS_CAP} ids (count is always the full total; truncated is true when any list was cut). ` +
13
+ 'Never retried. A sync that runs past the server operation timeout reports "timed out" and may still have imported, so re-list transactions before retrying. ' +
14
+ 'An all-accounts sync that fails on one account may still have imported the others; those imports reach the Actual server with the next write in this session or when it closes, so other clients may not see them yet.',
9
15
  inputSchema: InputSchema,
10
16
  call: async (args, _meta) => {
11
17
  const input = InputSchema.parse(args || {});
@@ -17,14 +23,21 @@ const tool = {
17
23
  if (input.accountId) {
18
24
  await adapter.resolveFilterId('account', input.accountId, { verifyExists: true });
19
25
  }
20
- try {
21
- await adapter.runBankSync(input.accountId ?? undefined);
22
- return { result: 'Bank sync initiated successfully' };
23
- }
24
- catch (error) {
25
- const msg = error instanceof Error ? error.message : String(error);
26
- throw new Error(msg);
27
- }
26
+ // #548: rethrow the adapter's error as is, so a typed refusal keeps its brand.
27
+ const report = await adapter.runBankSync(input.accountId ?? undefined);
28
+ let truncated = false;
29
+ const capped = (ids) => {
30
+ if (ids.length > BANK_SYNC_IDS_CAP)
31
+ truncated = true;
32
+ return { count: ids.length, ids: ids.slice(0, BANK_SYNC_IDS_CAP) };
33
+ };
34
+ return {
35
+ result: 'Bank sync completed',
36
+ newTransactions: capped(report.newTransactions),
37
+ matchedTransactions: capped(report.matchedTransactions),
38
+ updatedAccounts: capped(report.updatedAccounts),
39
+ truncated,
40
+ };
28
41
  },
29
42
  };
30
43
  export default tool;
@@ -52,6 +52,7 @@ export { default as rules_delete } from './rules_delete.js';
52
52
  export { default as rules_get } from './rules_get.js';
53
53
  export { default as rules_update } from './rules_update.js';
54
54
  export { default as schedules_create } from './schedules_create.js';
55
+ export { default as schedules_dates } from './schedules_dates.js';
55
56
  export { default as schedules_delete } from './schedules_delete.js';
56
57
  export { default as schedules_get } from './schedules_get.js';
57
58
  export { default as schedules_update } from './schedules_update.js';
@@ -0,0 +1,35 @@
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
+ import { SCHEDULE_DATES_CAP } from '../lib/constants.js';
6
+ export default createTool({
7
+ name: 'actual_schedules_dates',
8
+ description: 'List the dates one schedule falls on, computed by Actual itself, so month ends, patterns ' +
9
+ '("last Friday"), intervals and the weekend move match the Actual UI. Use this instead of ' +
10
+ 'expanding a schedule\'s recurrence yourself. Get schedule ids from actual_schedules_get. ' +
11
+ '`start` defaults to today; pass it whenever you pass `end`. With `end` and `count` both set, the list stops at whichever ' +
12
+ `comes first. At most ${SCHEDULE_DATES_CAP} dates are returned. \`truncated\` is true when more dates exist ` +
13
+ 'after the last one returned (within `end`, if given): ask again with a later `start` to continue. ' +
14
+ 'Returns { schedule_id, name, dates (ascending YYYY-MM-DD), count, truncated }.',
15
+ schema: z.object({
16
+ schedule_id: CommonSchemas.scheduleId,
17
+ start: CommonSchemas.date.optional().describe('First date to consider (YYYY-MM-DD); defaults to today'),
18
+ end: CommonSchemas.date.optional().describe('Last date to consider (YYYY-MM-DD), inclusive'),
19
+ count: z.number().int().min(1).max(SCHEDULE_DATES_CAP).optional()
20
+ .describe(`How many dates to return, 1 to ${SCHEDULE_DATES_CAP}`),
21
+ }),
22
+ handler: async ({ schedule_id, start, end, count }) => {
23
+ return await adapter.getScheduleDates(schedule_id, { start, end, count });
24
+ },
25
+ examples: [
26
+ {
27
+ description: 'Next three dates of a schedule',
28
+ input: { schedule_id: '00000000-0000-0000-0000-000000000001', count: 3 },
29
+ },
30
+ {
31
+ description: 'Every date of a schedule in Q4 2026',
32
+ input: { schedule_id: '00000000-0000-0000-0000-000000000001', start: '2026-10-01', end: '2026-12-31' },
33
+ },
34
+ ],
35
+ });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.25.1",
4
+ "version": "0.26.0",
5
5
  "mcpName": "io.github.agigante80/actual-mcp-server",
6
6
  "engines": {
7
7
  "node": ">=22.0.0",
8
8
  "npm": ">=10.0.0"
9
9
  },
10
- "description": "MCP server with 84 tools for AI-driven financial management with Actual Budget. HTTP and stdio transports for LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
10
+ "description": "MCP server with 85 tools for AI-driven financial management with Actual Budget. HTTP and stdio transports for LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
11
11
  "homepage": "https://github.com/agigante80/actual-mcp-server#readme",
12
12
  "repository": {
13
13
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "verify-tools": "npm run build && node scripts/verify-tools.js",
32
32
  "check:coverage": "node scripts/list-actual-api-methods.mjs",
33
33
  "direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
34
- "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/install_snippets.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/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.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/stdio_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/session_ownership.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/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.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_acl_listing.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/multi_server_budget_selection.test.js && node tests/unit/static_tokens.test.js && node tests/unit/httpServer_static_tokens.test.js && node tests/unit/httpServer_host_validation.test.js && node tests/unit/multi_server_budget_selection_stdio.test.js && node tests/unit/api_login_fetch.test.js && node tests/unit/multi_server_session_open_http.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/transactions_update_split_convert.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/transactions_update_batch.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/tool_content_logging.test.js && node tests/unit/adapter_error_logging.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/harness_cleanup.test.js && node tests/unit/harness_stdio_data_dir.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/rules_stage342_cleanup.test.js && node tests/unit/residue_unreadable.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/server_version_probe_outside_op_timeout.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 && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js && node tests/unit/session_admission_race.test.js && node tests/unit/pool_admission_reservation.test.js && node tests/unit/config_max_sessions_per_principal.test.js && node tests/unit/session_principal_cap.test.js && node tests/unit/schema_published_parity.test.js && node tests/unit/manual_borrowed_category.test.js && node tests/unit/manual_budget_no_borrowed_writes.test.js && node tests/unit/refresh_test_budget_snapshot.test.js && node tests/unit/docs_check.test.js && node tests/unit/docs_markers.test.js && node tests/unit/docs_links_fixture.test.js && node tests/unit/docs_index.test.js && node tests/unit/docs_tools.test.js && node tests/unit/docs_readme_size.test.js && node tests/unit/template_lockstep.test.js && node tests/unit/client_examples.test.js && node tests/unit/mcp_server_json.test.js",
34
+ "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/install_snippets.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/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.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/stdio_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/schedules_dates.test.js && node tests/unit/bank_sync.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/session_ownership.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/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.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_acl_listing.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/multi_server_budget_selection.test.js && node tests/unit/static_tokens.test.js && node tests/unit/httpServer_static_tokens.test.js && node tests/unit/httpServer_host_validation.test.js && node tests/unit/multi_server_budget_selection_stdio.test.js && node tests/unit/api_login_fetch.test.js && node tests/unit/multi_server_session_open_http.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/transactions_update_split_convert.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/transactions_update_batch.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/tool_content_logging.test.js && node tests/unit/adapter_error_logging.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/harness_cleanup.test.js && node tests/unit/harness_stdio_data_dir.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/rules_stage342_cleanup.test.js && node tests/unit/residue_unreadable.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/server_version_probe_outside_op_timeout.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 && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js && node tests/unit/session_admission_race.test.js && node tests/unit/pool_admission_reservation.test.js && node tests/unit/config_max_sessions_per_principal.test.js && node tests/unit/session_principal_cap.test.js && node tests/unit/schema_published_parity.test.js && node tests/unit/manual_borrowed_category.test.js && node tests/unit/manual_budget_no_borrowed_writes.test.js && node tests/unit/refresh_test_budget_snapshot.test.js && node tests/unit/docs_check.test.js && node tests/unit/docs_markers.test.js && node tests/unit/docs_links_fixture.test.js && node tests/unit/docs_index.test.js && node tests/unit/docs_tools.test.js && node tests/unit/docs_readme_size.test.js && node tests/unit/template_lockstep.test.js && node tests/unit/client_examples.test.js && node tests/unit/mcp_server_json.test.js",
35
35
  "test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
36
36
  "test:e2e": "npx playwright test",
37
37
  "test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
@@ -71,7 +71,7 @@
71
71
  "audit:write-effect": "node scripts/check-write-effect-audit.mjs"
72
72
  },
73
73
  "dependencies": {
74
- "@actual-app/api": "^26.10.0",
74
+ "@actual-app/api": "^26.11.0",
75
75
  "@modelcontextprotocol/sdk": "^1.32.2",
76
76
  "debug": "^4.4.3",
77
77
  "dotenv": "^18.0.9",