actual-mcp-server 0.25.2 → 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 +5 -5
- package/dist/src/actualToolsManager.js +1 -0
- package/dist/src/lib/actual-adapter.js +107 -64
- package/dist/src/lib/constants.js +8 -8
- package/dist/src/lib/schemas/common.js +1 -1
- package/dist/src/lib/tool-annotations.js +5 -4
- package/dist/src/lib/toolsets.js +2 -1
- package/dist/src/tools/bank_sync.js +22 -9
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/schedules_dates.js +35 -0
- package/package.json +3 -3
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
|
-
**
|
|
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
|
|
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` |
|
|
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
|
|
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.
|
|
172
|
+
**Version:** 0.26.0 | **Tool Count:** 85 (verified LibreChat-compatible)
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
3874
|
-
//
|
|
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
|
-
|
|
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
|
|
3887
|
-
|
|
3888
|
-
|
|
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
|
|
3897
|
-
if (
|
|
3898
|
-
throw new
|
|
3899
|
-
|
|
3900
|
-
`
|
|
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
|
-
//
|
|
3956
|
-
//
|
|
3957
|
-
|
|
3958
|
-
|
|
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 (
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
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
|
|
35
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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',
|
package/dist/src/lib/toolsets.js
CHANGED
|
@@ -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
|
|
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: '
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
package/dist/src/tools/index.js
CHANGED
|
@@ -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.
|
|
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
|
|
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",
|