actual-mcp-server 0.22.47 → 0.22.49
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 +1 -1
- package/dist/src/actualConnection.js +8 -8
- package/dist/src/lib/ActualConnectionPool.js +6 -12
- package/dist/src/lib/actual-adapter.js +75 -11
- package/dist/src/lib/apiLogin.js +75 -0
- package/dist/src/lib/apiState.js +28 -1
- package/dist/src/server/httpServer.js +10 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -836,7 +836,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
836
836
|
|
|
837
837
|
---
|
|
838
838
|
|
|
839
|
-
**Version:** 0.22.
|
|
839
|
+
**Version:** 0.22.49 | **Tool Count:** 84 (verified LibreChat-compatible)
|
|
840
840
|
|
|
841
841
|
## Sponsor
|
|
842
842
|
|
|
@@ -4,7 +4,7 @@ import os from 'os';
|
|
|
4
4
|
import api from '@actual-app/api';
|
|
5
5
|
import { loadBudgetTracked } from './lib/budgetLoader.js';
|
|
6
6
|
import { withApiLock } from './lib/apiLock.js';
|
|
7
|
-
import {
|
|
7
|
+
import { initApiLoggedIn } from './lib/apiLogin.js';
|
|
8
8
|
import logger from './logger.js';
|
|
9
9
|
import config from './config.js';
|
|
10
10
|
import { connectionPool } from './lib/ActualConnectionPool.js';
|
|
@@ -69,11 +69,7 @@ export async function connectToActual() {
|
|
|
69
69
|
// settle, `.finally(release)` would never run, and the process-global api mutex would be
|
|
70
70
|
// wedged permanently for every session: the exact #270 class. Both pool sites already bound
|
|
71
71
|
// their `api.init` this way, and this was the only `api.init` in the repo that did not.
|
|
72
|
-
await
|
|
73
|
-
dataDir: DATA_DIR,
|
|
74
|
-
serverURL: SERVER_URL,
|
|
75
|
-
password: PASSWORD,
|
|
76
|
-
}), 'connectToActual init');
|
|
72
|
+
await initApiLoggedIn(DATA_DIR, { serverUrl: SERVER_URL, password: PASSWORD || '' }, 'connectToActual init');
|
|
77
73
|
log.info(`Downloading budget with sync ID: ${BUDGET_SYNC_ID}`);
|
|
78
74
|
await loadBudgetTracked(BUDGET_SYNC_ID, BUDGET_PASSWORD);
|
|
79
75
|
});
|
|
@@ -132,8 +128,12 @@ export async function shutdownActual() {
|
|
|
132
128
|
/**
|
|
133
129
|
* Initialize connection for a specific MCP session
|
|
134
130
|
* Uses connection pooling to give each session its own Actual Budget connection
|
|
131
|
+
*
|
|
132
|
+
* #571: `budget` is the budget the session opens on (httpServer resolves it with
|
|
133
|
+
* initialBudgetForSession). Without it the pool logs in to the default server and downloads the
|
|
134
|
+
* default budget, which is wrong whenever the session's restored budget lives on another server.
|
|
135
135
|
*/
|
|
136
|
-
export async function connectToActualForSession(sessionId) {
|
|
136
|
+
export async function connectToActualForSession(sessionId, budget) {
|
|
137
137
|
if (!useConnectionPool) {
|
|
138
138
|
// Fallback to shared connection
|
|
139
139
|
return connectToActual();
|
|
@@ -141,7 +141,7 @@ export async function connectToActualForSession(sessionId) {
|
|
|
141
141
|
try {
|
|
142
142
|
// Ensure connection pool initialization is complete before accepting connections
|
|
143
143
|
await connectionPool.waitForInitialization();
|
|
144
|
-
await connectionPool.getConnection(sessionId);
|
|
144
|
+
await connectionPool.getConnection(sessionId, budget);
|
|
145
145
|
logger.info(`Actual API connection ready for session: ${sessionId}`);
|
|
146
146
|
}
|
|
147
147
|
catch (err) {
|
|
@@ -17,9 +17,9 @@ import path from 'path';
|
|
|
17
17
|
import os from 'os';
|
|
18
18
|
import fs from 'fs';
|
|
19
19
|
import { isApiInitialized, setApiInitialized } from './apiState.js';
|
|
20
|
+
import { initApiLoggedIn } from './apiLogin.js';
|
|
20
21
|
import { withApiLock, isApiLockHeld } from './apiLock.js';
|
|
21
22
|
import { loadBudgetTracked } from './budgetLoader.js';
|
|
22
|
-
import { withOpTimeout } from './opTimeout.js';
|
|
23
23
|
const DEFAULT_DATA_DIR = path.resolve(os.homedir() || '.', '.actual');
|
|
24
24
|
class ActualConnectionPool {
|
|
25
25
|
connections = new Map();
|
|
@@ -297,7 +297,9 @@ class ActualConnectionPool {
|
|
|
297
297
|
const SERVER_URL = budgetOverride?.serverUrl ?? config.ACTUAL_SERVER_URL;
|
|
298
298
|
const PASSWORD = budgetOverride?.password ?? config.ACTUAL_PASSWORD;
|
|
299
299
|
const BUDGET_SYNC_ID = budgetOverride?.syncId ?? config.ACTUAL_BUDGET_SYNC_ID;
|
|
300
|
-
|
|
300
|
+
// #571: an override carries its own budget's encryption password, absent included. Falling back
|
|
301
|
+
// to the default budget's would bind a budget from another server to a password it never had.
|
|
302
|
+
const BUDGET_PASSWORD = budgetOverride ? budgetOverride.encryptionPassword : process.env.ACTUAL_BUDGET_PASSWORD;
|
|
301
303
|
// Use shared data directory so changes persist across sessions
|
|
302
304
|
// This is critical: all sessions must share the same database to avoid data loss
|
|
303
305
|
const DATA_DIR = config.MCP_BRIDGE_DATA_DIR || DEFAULT_DATA_DIR;
|
|
@@ -322,11 +324,7 @@ class ActualConnectionPool {
|
|
|
322
324
|
// Bound the session-open init/download (#270): this is the HTTP exposure.
|
|
323
325
|
// A stalled upstream here would otherwise hang session open unbounded and,
|
|
324
326
|
// because the api singleton is process-global, wedge other sessions too.
|
|
325
|
-
await
|
|
326
|
-
dataDir: DATA_DIR,
|
|
327
|
-
serverURL: SERVER_URL,
|
|
328
|
-
password: PASSWORD,
|
|
329
|
-
}), 'pool init');
|
|
327
|
+
await initApiLoggedIn(DATA_DIR, { serverUrl: SERVER_URL, password: PASSWORD || '' }, 'pool init');
|
|
330
328
|
logger.info(`[ConnectionPool] Downloading budget for session: ${sessionId}`);
|
|
331
329
|
// #390: the shared tracked loader, not a hand-written download. It clears the record
|
|
332
330
|
// BEFORE starting and keeps an abandoned load registered, so a session-open download that
|
|
@@ -409,11 +407,7 @@ class ActualConnectionPool {
|
|
|
409
407
|
// process-global singleton, so it takes the same mutex.
|
|
410
408
|
await withApiLock(async () => {
|
|
411
409
|
// Bound the session-open init/download (#270), same as getConnection.
|
|
412
|
-
await
|
|
413
|
-
dataDir: DATA_DIR,
|
|
414
|
-
serverURL: SERVER_URL,
|
|
415
|
-
password: PASSWORD,
|
|
416
|
-
}), 'pool init');
|
|
410
|
+
await initApiLoggedIn(DATA_DIR, { serverUrl: SERVER_URL, password: PASSWORD || '' }, 'pool init');
|
|
417
411
|
// #390: the shared tracked loader, not a hand-written download. It clears the record
|
|
418
412
|
// BEFORE starting and keeps an abandoned load registered, so a session-open download that
|
|
419
413
|
// outruns ACTUAL_OP_TIMEOUT_MS cannot silently re-point the singleton later.
|
|
@@ -23,7 +23,8 @@ import { parseBudgetRegistry } from './budget-registry.js';
|
|
|
23
23
|
import { getPreferredBudgetSyncId, setPreferredBudgetSyncId, pickAllowedPreferredBudget } from './budget-preference-store.js';
|
|
24
24
|
import { requestContext } from './requestContext.js';
|
|
25
25
|
import { connectionPool } from './ActualConnectionPool.js';
|
|
26
|
-
import { isApiInitialized, setApiInitialized, getLoadedBudgetSyncId, awaitAbandonedBudgetLoad, hasPendingBudgetLoad } from './apiState.js';
|
|
26
|
+
import { isApiInitialized, setApiInitialized, getLoadedBudgetSyncId, awaitAbandonedBudgetLoad, hasPendingBudgetLoad, isApiLoggedInTo, setLoginsSpanServers } from './apiState.js';
|
|
27
|
+
import { initApiLoggedIn } from './apiLogin.js';
|
|
27
28
|
import { withApiLock } from './apiLock.js';
|
|
28
29
|
import { loadBudgetTracked, importBudgetTracked } from './budgetLoader.js';
|
|
29
30
|
/**
|
|
@@ -36,6 +37,7 @@ const budgetRegistry = parseBudgetRegistry(process.env, {
|
|
|
36
37
|
syncId: config.ACTUAL_BUDGET_SYNC_ID,
|
|
37
38
|
encryptionPassword: config.ACTUAL_BUDGET_PASSWORD,
|
|
38
39
|
});
|
|
40
|
+
setLoginsSpanServers(new Set([...budgetRegistry.values()].map((b) => JSON.stringify([b.serverUrl, b.password || '']))).size > 1);
|
|
39
41
|
logger.info(`[ADAPTER] Budget registry: ${budgetRegistry.size} budget(s) — ` +
|
|
40
42
|
[...budgetRegistry.values()].map(b => `"${b.name}" (${b.serverUrl})`).join(', '));
|
|
41
43
|
/**
|
|
@@ -108,6 +110,25 @@ function getActiveBudgetConfig() {
|
|
|
108
110
|
}
|
|
109
111
|
return [...budgetRegistry.values()][0];
|
|
110
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* #571: the budget a NEW session should open on, resolved before its session id is live.
|
|
115
|
+
*
|
|
116
|
+
* httpServer opens the pool connection from `onsessioninitialized`, which runs inside the
|
|
117
|
+
* initialize request's context: principal and ACL are there, the session id is not yet. Without
|
|
118
|
+
* the id, getActiveBudgetConfig cannot restore the #189 preference, so the connection logged in
|
|
119
|
+
* to the DEFAULT server and downloaded the default budget even when the session's budget lives
|
|
120
|
+
* on another server, and the session's first call then asked the default server for it.
|
|
121
|
+
*
|
|
122
|
+
* Runs getActiveBudgetConfig with the id added to the CURRENT context, so the ACL and principal
|
|
123
|
+
* of the initialize request still decide. Its memoisation goes through setSessionBudget, which
|
|
124
|
+
* the isSessionLive guard keeps from recording a session that already ended.
|
|
125
|
+
*
|
|
126
|
+
* Shape credit: @jospaquim (PR #569).
|
|
127
|
+
*/
|
|
128
|
+
export function initialBudgetForSession(sessionId) {
|
|
129
|
+
const store = requestContext.getStore() ?? {};
|
|
130
|
+
return requestContext.run({ ...store, sessionId }, () => getActiveBudgetConfig());
|
|
131
|
+
}
|
|
111
132
|
/**
|
|
112
133
|
* Global API session mutex.
|
|
113
134
|
* @actual-app/api is a singleton with a single SQLite connection — concurrent
|
|
@@ -172,7 +193,10 @@ async function ensureLoadedBudgetMatchesSession() {
|
|
|
172
193
|
}
|
|
173
194
|
const wanted = getActiveBudgetConfig();
|
|
174
195
|
const loaded = getLoadedBudgetSyncId();
|
|
175
|
-
|
|
196
|
+
// #571: the sync id alone is not a match. The same sync id can exist on two servers (a budget
|
|
197
|
+
// copied between them), and a match against the wrong server would run this session's
|
|
198
|
+
// operation on the other server's copy.
|
|
199
|
+
if (loaded === wanted.syncId && isApiLoggedInTo(loginOf(wanted)))
|
|
176
200
|
return;
|
|
177
201
|
// Deliberately a WARN: reaching here means two sessions are on different budgets and this
|
|
178
202
|
// one would otherwise have operated on the other's data.
|
|
@@ -185,7 +209,11 @@ async function ensureLoadedBudgetMatchesSession() {
|
|
|
185
209
|
// per operation, a drain can now change budgets MID-BATCH, and the single trailing api.sync()
|
|
186
210
|
// covers only the last one. Without this, an earlier session's write is applied locally and
|
|
187
211
|
// silently waits for the next load of that budget to propagate, which an ephemeral data dir
|
|
188
|
-
// would lose entirely.
|
|
212
|
+
// would lose entirely. Runs on a cross-login re-selection too, while the old login is still
|
|
213
|
+
// current: initApiLoggedIn's close deliberately does not sync (see there for the out-of-sync
|
|
214
|
+
// failure a syncing close caused at session open), so this is the only sync the outgoing budget
|
|
215
|
+
// gets. It is still a sync of a copy that may be stale if another process wrote the data dir:
|
|
216
|
+
// #575 tracks that hazard, which every sync here and after writes shares.
|
|
189
217
|
if (loaded) {
|
|
190
218
|
try {
|
|
191
219
|
await withOpTimeout(() => api.sync(), 'sync');
|
|
@@ -197,7 +225,45 @@ async function ensureLoadedBudgetMatchesSession() {
|
|
|
197
225
|
});
|
|
198
226
|
}
|
|
199
227
|
}
|
|
200
|
-
await
|
|
228
|
+
await loadBudgetOnItsServer(wanted);
|
|
229
|
+
}
|
|
230
|
+
function loginOf(budget) {
|
|
231
|
+
return { serverUrl: budget.serverUrl, password: budget.password || '' };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* #571: load a budget over a login to the server that HOLDS it. MUST be called inside the api lock.
|
|
235
|
+
*
|
|
236
|
+
* Every re-selection over a live singleton goes through here (the #390 precondition, which the
|
|
237
|
+
* kept-alive stdio singleton of #419 reaches too, and switchBudget's same-auth fast path). Both
|
|
238
|
+
* used to download over whatever login the singleton already had, which is right on a single
|
|
239
|
+
* server and wrong as soon as another session has logged the singleton in to a different one:
|
|
240
|
+
* reproduced live as a session on server X failing every call with "Budget not found" for as
|
|
241
|
+
* long as a session on server Y was active.
|
|
242
|
+
*
|
|
243
|
+
* Same server: no new login, so the #127 auth burst #134 removed does not come back.
|
|
244
|
+
*
|
|
245
|
+
* Different or unknown server: log in to the budget's own server with the budget's own password
|
|
246
|
+
* through initApiLoggedIn, which first clears the records and closes the outgoing budget WITHOUT
|
|
247
|
+
* syncing it, then download. The #390 precondition syncs the outgoing budget before calling here;
|
|
248
|
+
* switchBudget's same-auth fast path does not, so a pending change there syncs at that budget's
|
|
249
|
+
* next load.
|
|
250
|
+
*
|
|
251
|
+
* Failure: initApiLoggedIn clears the loaded-budget and login records before anything else, so
|
|
252
|
+
* any failure leaves both unknown, the next operation of ANY session re-selects, and the error
|
|
253
|
+
* reaches the caller as an ordinary tool error. The live flag is deliberately NOT cleared:
|
|
254
|
+
* clearing it would send every session through the legacy per-operation init (the #127 burst)
|
|
255
|
+
* until a new session happened to open, while the cleared records already force the one re-login
|
|
256
|
+
* that recovery needs.
|
|
257
|
+
*
|
|
258
|
+
* Never takes the api lock itself: the lock is not reentrant and every caller already holds it.
|
|
259
|
+
*/
|
|
260
|
+
async function loadBudgetOnItsServer(budget) {
|
|
261
|
+
const login = loginOf(budget);
|
|
262
|
+
if (!isApiLoggedInTo(login)) {
|
|
263
|
+
logger.info(`[ADAPTER] logging the api in to ${budget.serverUrl} before loading "${budget.name}"`);
|
|
264
|
+
await withAuthRetry(() => initApiLoggedIn(config.MCP_BRIDGE_DATA_DIR, login, 'init'));
|
|
265
|
+
}
|
|
266
|
+
await loadBudgetTracked(budget.syncId, budget.encryptionPassword);
|
|
201
267
|
}
|
|
202
268
|
// Per-op timeout (#270) lives in ./opTimeout.ts so ActualConnectionPool can bound
|
|
203
269
|
// its own session-open init/download without a circular import back into this
|
|
@@ -741,11 +807,7 @@ async function initActualApiForOperation() {
|
|
|
741
807
|
// withAuthRetry does not retry it. Wrapping per-attempt (not the whole retry
|
|
742
808
|
// loop) means legitimate #127 auth-rate-limit backoff (up to ~25s) is not
|
|
743
809
|
// counted against ACTUAL_OP_TIMEOUT_MS.
|
|
744
|
-
await withAuthRetry(() =>
|
|
745
|
-
dataDir: DATA_DIR,
|
|
746
|
-
serverURL: budget.serverUrl,
|
|
747
|
-
password: budget.password || '',
|
|
748
|
-
}), 'init'));
|
|
810
|
+
await withAuthRetry(() => initApiLoggedIn(DATA_DIR, loginOf(budget), 'init'));
|
|
749
811
|
logger.debug('[ADAPTER] Downloading budget');
|
|
750
812
|
// Bound downloadBudget (#270): the legacy stdio path re-downloads on every
|
|
751
813
|
// op, and a stall here was the production hang. On timeout it rejects and
|
|
@@ -4005,8 +4067,10 @@ export async function switchBudget(name) {
|
|
|
4005
4067
|
}
|
|
4006
4068
|
else {
|
|
4007
4069
|
await withApiLock(async () => {
|
|
4008
|
-
// Bound the in-place budget reload too (#270).
|
|
4009
|
-
|
|
4070
|
+
// Bound the in-place budget reload too (#270). #571: "same auth" compares against THIS
|
|
4071
|
+
// session's pool entry, but the singleton may be logged in to another session's server by
|
|
4072
|
+
// now, so the load still has to make sure it is talking to this budget's server.
|
|
4073
|
+
await loadBudgetOnItsServer(found);
|
|
4010
4074
|
});
|
|
4011
4075
|
}
|
|
4012
4076
|
connectionPool.updateLoadedSyncId(sessionId, found.syncId);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import api from '@actual-app/api';
|
|
2
|
+
import { withOpTimeout } from './opTimeout.js';
|
|
3
|
+
import { setApiLogin, setLoadedBudgetSyncId, registerBudgetLoad, loginsSpanServers } from './apiState.js';
|
|
4
|
+
// Upstream's `init` returns its module-level handler object, the same one every time. Upstream
|
|
5
|
+
// exports no other way to reach `close-budget`. Nothing can be open before the first init, which
|
|
6
|
+
// is when this is still unset.
|
|
7
|
+
let upstreamLib = null;
|
|
8
|
+
/**
|
|
9
|
+
* #571: the ONLY place in src/ that calls `api.init`.
|
|
10
|
+
*
|
|
11
|
+
* With a multi-server budget registry, whether the singleton may skip a login depends on
|
|
12
|
+
* knowing which server (and password) it is logged in to, and an unknown login counts as a
|
|
13
|
+
* mismatch. A site that called `api.init` without recording the login would therefore not be
|
|
14
|
+
* wrong, only slow: every later operation would log in again, which is the #127 auth burst that
|
|
15
|
+
* #134 removed. Routing every init through here makes "logged in" and "recorded" one step, and
|
|
16
|
+
* `tests/unit/multi_server_budget_selection.test.js` fails on any raw `api.init(` elsewhere.
|
|
17
|
+
*
|
|
18
|
+
* Order, every time:
|
|
19
|
+
* 1. clear the login and loaded-budget records. Upstream's init points the client at the new
|
|
20
|
+
* server before it signs in, so a login that throws or times out leaves the client on a
|
|
21
|
+
* server with nothing proven; clearing first means nothing trusts it afterwards, and the
|
|
22
|
+
* next operation of any session re-selects.
|
|
23
|
+
* 2. close any open budget with upstream's `close-budget`, which does NOT sync. Upstream's init
|
|
24
|
+
* does not close an open budget, so without this a pending change of the outgoing budget
|
|
25
|
+
* would next try to sync against the new server. Harmless when nothing is open (the legacy
|
|
26
|
+
* per-operation cycle has already shut down). Never `api.shutdown`: it syncs before it
|
|
27
|
+
* closes, and syncing an in-memory copy that is stale against its own data dir (another
|
|
28
|
+
* process wrote that budget) fails "out-of-sync" and leaves the local copy permanently
|
|
29
|
+
* unopenable; every session open on that budget then fails until the copy is deleted. The
|
|
30
|
+
* v0.22.48 full run hit exactly that after the stdio #270 regression, which v0.22.47 survived
|
|
31
|
+
* because its session open only ran upstream's download, whose own close does not sync
|
|
32
|
+
* either. This removes only the session-open trigger: the adapter precondition still syncs
|
|
33
|
+
* the outgoing budget before re-selecting (so a pending change is not lost), and that sync
|
|
34
|
+
* carries the same stale-copy hazard when a second process writes the data dir (#575).
|
|
35
|
+
* Anything still pending syncs at that budget's next load. Not the pool's shutdown helpers: those drop pool entries, and the other sessions'
|
|
36
|
+
* entries are still valid.
|
|
37
|
+
* 3. log in, then record the login.
|
|
38
|
+
*
|
|
39
|
+
* Bounded by the op timeout (#270). Callers that retry on the auth rate limit wrap this whole
|
|
40
|
+
* call in `withAuthRetry`, so each attempt is bounded on its own.
|
|
41
|
+
*
|
|
42
|
+
* A timeout does not cancel (`withOpTimeout` races), so a timed-out close or login keeps running
|
|
43
|
+
* after the caller has thrown and released the lock. A late close would close the budget the next
|
|
44
|
+
* operation loaded, in any deployment, so it is always registered with the #393 abandoned-load
|
|
45
|
+
* registry: every `withApiLock` acquisition settles it, bounded and failing closed, before it runs
|
|
46
|
+
* anything. A late login is registered only when the registry names more than one login
|
|
47
|
+
* (`loginsSpanServers`): there it could move the client to another server under the next
|
|
48
|
+
* operation, while with one login it lands on the only server there is, and registering it would
|
|
49
|
+
* replace #270's per-request recovery with a fail-closed wait on every operation. The close
|
|
50
|
+
* has no such exemption: it waits on upstream's spreadsheet and services (not on running handlers), so a close
|
|
51
|
+
* that stalls makes every session wait, bounded and failing closed, in a one-login deployment
|
|
52
|
+
* too (the accepted #393 trade-off; making the close and login abortable is #574). Their late
|
|
53
|
+
* success records nothing (the records were cleared in step 1 and `setApiLogin` is only reached by
|
|
54
|
+
* a caller still waiting), so the next operation logs in again.
|
|
55
|
+
*/
|
|
56
|
+
export async function initApiLoggedIn(dataDir, login, label) {
|
|
57
|
+
setApiLogin(null);
|
|
58
|
+
setLoadedBudgetSyncId(null);
|
|
59
|
+
if (upstreamLib) {
|
|
60
|
+
const closing = upstreamLib.send('close-budget');
|
|
61
|
+
registerBudgetLoad(closing);
|
|
62
|
+
await withOpTimeout(() => closing, `${label} (close outgoing budget)`);
|
|
63
|
+
}
|
|
64
|
+
const loggingIn = api.init({
|
|
65
|
+
dataDir,
|
|
66
|
+
serverURL: login.serverUrl,
|
|
67
|
+
password: login.password,
|
|
68
|
+
});
|
|
69
|
+
if (loginsSpanServers())
|
|
70
|
+
registerBudgetLoad(loggingIn);
|
|
71
|
+
const lib = await withOpTimeout(() => loggingIn, label);
|
|
72
|
+
if (lib && typeof lib.send === 'function')
|
|
73
|
+
upstreamLib = lib;
|
|
74
|
+
setApiLogin(login);
|
|
75
|
+
}
|
package/dist/src/lib/apiState.js
CHANGED
|
@@ -25,8 +25,35 @@ export function setApiInitialized(value) {
|
|
|
25
25
|
_apiInitialized = value;
|
|
26
26
|
// A singleton that is not live holds no budget. Clearing here means a shutdown can never
|
|
27
27
|
// leave a stale claim behind for the next caller to trust.
|
|
28
|
-
if (!value)
|
|
28
|
+
if (!value) {
|
|
29
29
|
_loadedBudgetSyncId = null;
|
|
30
|
+
_apiLogin = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let _apiLogin = null;
|
|
34
|
+
export function setApiLogin(login) {
|
|
35
|
+
_apiLogin = login ? { serverUrl: login.serverUrl, password: login.password } : null;
|
|
36
|
+
}
|
|
37
|
+
export function isApiLoggedInTo(login) {
|
|
38
|
+
return !!_apiLogin && _apiLogin.serverUrl === login.serverUrl && _apiLogin.password === login.password;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* #571: whether the budget registry names more than one login (server URL and password).
|
|
42
|
+
*
|
|
43
|
+
* Decides whether a timed-out `api.init` is registered as an abandoned load. With ONE login a
|
|
44
|
+
* late-landing init points the client at the only server there is, which is harmless, and
|
|
45
|
+
* registering it would turn one stalled login into a fail-closed wait on every later operation
|
|
46
|
+
* until upstream gave up (the per-request recovery #270 guarantees, checked by
|
|
47
|
+
* tests/unit/adapter_op_timeout.test.js). With several, a late landing can move the client to
|
|
48
|
+
* another server under the next operation, so it must be settled first. Set once, by the adapter,
|
|
49
|
+
* from the parsed registry.
|
|
50
|
+
*/
|
|
51
|
+
let _loginsSpanServers = false;
|
|
52
|
+
export function setLoginsSpanServers(value) {
|
|
53
|
+
_loginsSpanServers = value;
|
|
54
|
+
}
|
|
55
|
+
export function loginsSpanServers() {
|
|
56
|
+
return _loginsSpanServers;
|
|
30
57
|
}
|
|
31
58
|
/**
|
|
32
59
|
* #390: the syncId of the budget the SINGLETON currently holds, as distinct from the budget
|
|
@@ -9,7 +9,7 @@ import { getLocalIp } from '../utils.js';
|
|
|
9
9
|
import actualToolsManager from '../actualToolsManager.js';
|
|
10
10
|
import { getConnectionState, connectToActualForSession, shutdownActualForSession, shutdownActual, canAcceptNewSession, isConnectionPoolEnabled } from '../actualConnection.js';
|
|
11
11
|
import { connectionPool } from '../lib/ActualConnectionPool.js';
|
|
12
|
-
import { clearSessionBudgetState, setSessionLivenessCheck } from '../lib/actual-adapter.js';
|
|
12
|
+
import { clearSessionBudgetState, setSessionLivenessCheck, initialBudgetForSession } from '../lib/actual-adapter.js';
|
|
13
13
|
import { errorLogFields } from '../lib/errors.js';
|
|
14
14
|
import observability from '../observability.js';
|
|
15
15
|
import config from '../config.js';
|
|
@@ -538,7 +538,15 @@ advertisedUrl) {
|
|
|
538
538
|
sessionInitPromises.set(sid, initPromise);
|
|
539
539
|
// Initialize connection pool for this session
|
|
540
540
|
try {
|
|
541
|
-
|
|
541
|
+
// #571: open on the session's own budget (a restored #189 preference may live on
|
|
542
|
+
// another server), not on the default one.
|
|
543
|
+
const budget = initialBudgetForSession(sid);
|
|
544
|
+
await connectToActualForSession(sid, {
|
|
545
|
+
serverUrl: budget.serverUrl,
|
|
546
|
+
password: budget.password || '',
|
|
547
|
+
syncId: budget.syncId,
|
|
548
|
+
encryptionPassword: budget.encryptionPassword,
|
|
549
|
+
});
|
|
542
550
|
// Only register the transport if the pool connection succeeded.
|
|
543
551
|
// The pool stamped lastActivity when it created the entry, so it
|
|
544
552
|
// is already the source of truth for this session's idle clock.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.22.
|
|
4
|
+
"version": "0.22.49",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
31
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
32
|
"direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
|
|
33
|
-
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/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/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/batch_uncategorized_cleanup.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",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/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/multi_server_budget_selection_stdio.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/batch_uncategorized_cleanup.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",
|
|
34
34
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
35
|
"test:e2e": "npx playwright test",
|
|
36
36
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|