actual-mcp-server 0.26.0 → 0.26.1
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/config.js +4 -0
- package/dist/src/index.js +29 -53
- package/dist/src/lib/actual-adapter.js +15 -11
- package/dist/src/lib/apiLogin.js +7 -1
- package/dist/src/lib/authPosture.js +17 -0
- package/dist/src/lib/budget-registry.js +39 -1
- package/dist/src/lib/mcp-version.js +6 -0
- package/dist/src/lib/processHandlers.js +183 -0
- package/dist/src/lib/processHandlersInstall.js +25 -0
- package/dist/src/lib/rejection-allowlist.js +4 -3
- package/dist/src/server/httpServer.js +7 -4
- package/dist/src/server/stdioServer.js +5 -2
- package/dist/src/tools/budgets_list_available.js +1 -1
- package/dist/src/tools/schedules_update.js +6 -4
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -89,6 +89,10 @@ export const configSchema = z.object({
|
|
|
89
89
|
// password is set (e.g. an isolated Docker network where the hop is trusted).
|
|
90
90
|
// Off by default so a plaintext upstream + encryption password is refused.
|
|
91
91
|
ALLOW_INSECURE_UPSTREAM: z.string().optional().transform(val => val === 'true'),
|
|
92
|
+
// #620: upstream @actual-app/api logs at its own info/debug level (sync chatter, and on every
|
|
93
|
+
// import or bank sync a dump of each reconciled transaction) unless init is told otherwise.
|
|
94
|
+
// Off by default; `=== 'true'` fails safe, since turning it on is what carries the risk.
|
|
95
|
+
ACTUAL_API_VERBOSE: z.string().optional().transform(val => val === 'true'),
|
|
92
96
|
MCP_BRIDGE_DATA_DIR: z.string().default('./actual-data'),
|
|
93
97
|
// #332: where actual_budgets_export writes budget zips. A budget export is
|
|
94
98
|
// megabytes of binary, so the tool returns a PATH plus metadata rather than
|
package/dist/src/index.js
CHANGED
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
// guard cannot live in bin/ alone. It is a side-effect import: the module's top-level
|
|
6
6
|
// `enforceNodeVersion()` fires here, exactly once.
|
|
7
7
|
import './lib/node-version-guard.js';
|
|
8
|
+
// #622: MUST be the second import. Registers the unhandledRejection and uncaughtException
|
|
9
|
+
// handlers before any other module evaluates. They write gated records only (no reason,
|
|
10
|
+
// message or stack) and get the logger later through attachProcessLogger below.
|
|
11
|
+
import { attachProcessLogger } from './lib/processHandlersInstall.js';
|
|
8
12
|
import { getMcpServerVersion } from './lib/mcp-version.js';
|
|
9
13
|
import path from 'node:path';
|
|
10
14
|
import { z } from 'zod';
|
|
11
|
-
import { isKnownBenignRejection } from './lib/rejection-allowlist.js';
|
|
12
15
|
import { buildServerInstructions } from './lib/serverInstructions.js';
|
|
13
16
|
import { format } from 'node:util';
|
|
17
|
+
import { writeSync } from 'node:fs';
|
|
14
18
|
/*
|
|
15
19
|
* #277 / #541: this module's version comes from `getMcpServerVersion()` (src/lib/mcp-version.ts),
|
|
16
20
|
* which `--version`, `--help`, the startup banner, the MCP initialize `serverInfo.version` and
|
|
@@ -22,54 +26,6 @@ import { format } from 'node:util';
|
|
|
22
26
|
* ignored `VERSION`, so a Docker image built as `X-dev-abc` printed plain `X` from
|
|
23
27
|
* `--version` while clients saw `X-dev-abc`. One reader means one string.
|
|
24
28
|
*/
|
|
25
|
-
// Add global error handlers
|
|
26
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
27
|
-
console.error('=== UNHANDLED REJECTION ===');
|
|
28
|
-
console.error('Promise:', promise);
|
|
29
|
-
console.error('Reason:', reason);
|
|
30
|
-
if (reason instanceof Error) {
|
|
31
|
-
console.error('Stack:', reason.stack);
|
|
32
|
-
}
|
|
33
|
-
console.error('===========================');
|
|
34
|
-
// #275 backstop. The guard above catches both entry points, but a consumer that
|
|
35
|
-
// imports this package as a library reaches neither. Import attributes are the one
|
|
36
|
-
// construct here that fails on an under-floor Node, and it fails asynchronously,
|
|
37
|
-
// so name the real cause rather than letting the reader chase a packaging bug.
|
|
38
|
-
if (reason?.code === 'ERR_IMPORT_ASSERTION_TYPE_MISSING') {
|
|
39
|
-
console.error(`⚠️ This almost certainly means Node ${process.version} is too old.`);
|
|
40
|
-
console.error('⚠️ actual-mcp-server requires the Node version in package.json "engines".');
|
|
41
|
-
console.error('⚠️ Import attributes (`with { type: "json" }`) need Node 22 or newer.');
|
|
42
|
-
}
|
|
43
|
-
if (isKnownBenignRejection(reason)) {
|
|
44
|
-
console.error('⚠️ Known Actual API domain error escaped to unhandledRejection:');
|
|
45
|
-
console.error('⚠️ ' + String(reason));
|
|
46
|
-
console.error('⚠️ Server will continue running. The caller received an error response.');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
process.exit(1);
|
|
50
|
-
});
|
|
51
|
-
process.on('uncaughtException', (error) => {
|
|
52
|
-
const errMsg = String(error?.message || error);
|
|
53
|
-
const errType = error?.type;
|
|
54
|
-
// Bank sync errors from GoCardless/SimpleFIN can surface as uncaughtException
|
|
55
|
-
// from within the @actual-app/api SDK when the provider API returns an error
|
|
56
|
-
// asynchronously (e.g. RATE_LIMIT_EXCEEDED from GoCardless/Nordigen).
|
|
57
|
-
// These are non-fatal — the server should survive and continue serving requests.
|
|
58
|
-
if (errType === 'BankSyncError' ||
|
|
59
|
-
errMsg.includes('BankSyncError') ||
|
|
60
|
-
errMsg.includes('NORDIGEN_ERROR') ||
|
|
61
|
-
errMsg.includes('RATE_LIMIT_EXCEEDED') ||
|
|
62
|
-
errMsg.includes('Rate limit exceeded') ||
|
|
63
|
-
errMsg.includes('Failed syncing account') ||
|
|
64
|
-
errMsg.includes('GoCardless') ||
|
|
65
|
-
errMsg.includes('SimpleFIN')) {
|
|
66
|
-
console.error('⚠️ [BANK SYNC] Non-fatal bank sync error surfaced as uncaughtException (server continues):');
|
|
67
|
-
console.error('⚠️ ' + errMsg);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
console.error('Uncaught Exception:', error);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
});
|
|
73
29
|
// Minimal early help handling before any side-effectful modules (prevents dotenv from running on --help)
|
|
74
30
|
const argsEarly = process.argv.slice(2);
|
|
75
31
|
// Set MCP_STDIO_MODE before the async IIFE so it is in place when src/logger.ts is first
|
|
@@ -191,7 +147,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
191
147
|
import('./actualConnection.js'),
|
|
192
148
|
import('./lib/ActualMCPConnection.js'),
|
|
193
149
|
]);
|
|
194
|
-
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture, findWeakTokenVars }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
150
|
+
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture, findWeakTokenVars, describeAuthMode }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
195
151
|
import('./server/httpServer.js'),
|
|
196
152
|
import('./server/stdioServer.js'),
|
|
197
153
|
import('./lib/authPosture.js'),
|
|
@@ -200,6 +156,9 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
200
156
|
import('./actualToolsManager.js'),
|
|
201
157
|
]);
|
|
202
158
|
const logger = loggerModule.default;
|
|
159
|
+
// #622: the process handlers log survivable events through the logger from here on.
|
|
160
|
+
const { createModuleLogger } = await import('./lib/loggerFactory.js');
|
|
161
|
+
attachProcessLogger(createModuleLogger('PROCESS'));
|
|
203
162
|
const { getLocalIp } = utilsModule;
|
|
204
163
|
const actualToolsManager = actualToolsManagerModule.default;
|
|
205
164
|
// #541: the build-time VERSION, or the root package version plus a dev suffix on a
|
|
@@ -213,7 +172,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
213
172
|
const useStdio = args.includes('--stdio');
|
|
214
173
|
const useTestActualConnection = args.includes('--test-actual-connection');
|
|
215
174
|
const useTestMcpClient = args.includes('--test-mcp-client');
|
|
216
|
-
const SERVER_DESCRIPTION = '
|
|
175
|
+
const SERVER_DESCRIPTION = 'Actual Budget over the Model Context Protocol';
|
|
217
176
|
async function main() {
|
|
218
177
|
// Mutual exclusion — stdio and http are incompatible transports
|
|
219
178
|
if (useHttp && useStdio) {
|
|
@@ -359,7 +318,16 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
359
318
|
logger.info('---------');
|
|
360
319
|
logger.info('🟡 MCP SERVER INFO');
|
|
361
320
|
logger.info(`• Server Description: ${SERVER_DESCRIPTION}`);
|
|
362
|
-
|
|
321
|
+
// #626: the real mode, computed with the same blank-means-off rules as the
|
|
322
|
+
// posture check. MCP_SSE_AUTHORIZATION stays untrimmed: a whitespace-only
|
|
323
|
+
// value is still a live credential there.
|
|
324
|
+
const { staticTokensConfigured } = await import('./auth/static-tokens.js');
|
|
325
|
+
const authMode = describeAuthMode({
|
|
326
|
+
oidcEnabled: process.env.AUTH_PROVIDER === 'oidc',
|
|
327
|
+
hasStaticTokens: staticTokensConfigured(process.env.MCP_STATIC_TOKENS),
|
|
328
|
+
hasSseAuthorization: !!process.env.MCP_SSE_AUTHORIZATION,
|
|
329
|
+
});
|
|
330
|
+
logger.info(`• Auth mode: ${authMode}`);
|
|
363
331
|
logger.info(`• Capabilities: ${Object.keys(capabilities).join(', ')}`);
|
|
364
332
|
logger.info(`• Tools: ${implementedTools.join(', ') || 'none'}`);
|
|
365
333
|
logger.info(`• Server Instructions: ${serverInstructionsText}`);
|
|
@@ -390,4 +358,12 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
390
358
|
logger.error(`Failed to start Actual MCP bridge: ${err?.message || String(err)}`);
|
|
391
359
|
process.exit(1);
|
|
392
360
|
});
|
|
393
|
-
})()
|
|
361
|
+
})().catch((err) => {
|
|
362
|
+
// #622: the prelude above (dotenv, the dynamic imports, the logger) runs before main(), so
|
|
363
|
+
// its failures never reach main().catch. Without this catch they escape as an
|
|
364
|
+
// unhandledRejection, whose gated record drops the message: a missing package or a broken
|
|
365
|
+
// build would name no module. Same residual as main().catch: startup text only. The logger
|
|
366
|
+
// may not exist yet, so this writes synchronously to stderr, never stdout.
|
|
367
|
+
writeSync(2, `Failed to start Actual MCP bridge: ${err?.message || String(err)}\n`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
});
|
|
@@ -19,7 +19,7 @@ import { findMatchingRule } from './rule-matching.js';
|
|
|
19
19
|
import logger from '../logger.js';
|
|
20
20
|
import { checkServerVersionOnce } from './server-version-guard.js';
|
|
21
21
|
import config from '../config.js';
|
|
22
|
-
import { parseBudgetRegistry } from './budget-registry.js';
|
|
22
|
+
import { formatBudgetRegistrySummary, logSafeOrigin, 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';
|
|
@@ -38,8 +38,8 @@ const budgetRegistry = parseBudgetRegistry(process.env, {
|
|
|
38
38
|
encryptionPassword: config.ACTUAL_BUDGET_PASSWORD,
|
|
39
39
|
});
|
|
40
40
|
setLoginsSpanServers(new Set([...budgetRegistry.values()].map((b) => JSON.stringify([b.serverUrl, b.password || '']))).size > 1);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// #621: counts and schemes only; budget names and server hosts stay out of the startup log.
|
|
42
|
+
logger.info(`[ADAPTER] ${formatBudgetRegistrySummary(budgetRegistry)}`);
|
|
43
43
|
/**
|
|
44
44
|
* Per-session active budget. Map from sessionId to lowercased budget key
|
|
45
45
|
* (matches the keys in budgetRegistry).
|
|
@@ -276,7 +276,7 @@ function loginOf(budget) {
|
|
|
276
276
|
async function loadBudgetOnItsServer(budget) {
|
|
277
277
|
const login = loginOf(budget);
|
|
278
278
|
if (!isApiLoggedInTo(login)) {
|
|
279
|
-
logger.info(`[ADAPTER] logging the api in to ${budget.serverUrl} before loading "${budget.name}"`);
|
|
279
|
+
logger.info(`[ADAPTER] logging the api in to ${logSafeOrigin(budget.serverUrl)} before loading "${budget.name}"`);
|
|
280
280
|
await withAuthRetry(() => initApiLoggedIn(config.MCP_BRIDGE_DATA_DIR, login, 'init'));
|
|
281
281
|
}
|
|
282
282
|
await loadBudgetTracked(budget.syncId, budget.encryptionPassword);
|
|
@@ -828,7 +828,7 @@ async function initActualApiForOperation() {
|
|
|
828
828
|
try {
|
|
829
829
|
const budget = getActiveBudgetConfig();
|
|
830
830
|
const DATA_DIR = config.MCP_BRIDGE_DATA_DIR;
|
|
831
|
-
logger.debug(`[ADAPTER] Initializing Actual API for operation (budget: "${budget.name}", server: ${budget.serverUrl})`);
|
|
831
|
+
logger.debug(`[ADAPTER] Initializing Actual API for operation (budget: "${budget.name}", server: ${logSafeOrigin(budget.serverUrl)})`);
|
|
832
832
|
// Wrap api.init in auth-rate-limit retry so a transient too-many-requests
|
|
833
833
|
// doesn't surface to the caller (and doesn't trigger #132's crash path).
|
|
834
834
|
// Bound EACH init attempt with the op timeout (#270), inside withAuthRetry:
|
|
@@ -4078,6 +4078,9 @@ export async function switchBudget(name) {
|
|
|
4078
4078
|
};
|
|
4079
4079
|
if (!found)
|
|
4080
4080
|
throw notFound();
|
|
4081
|
+
// #628: tool results show the server origin only, never a userinfo credential, path or query.
|
|
4082
|
+
// `found` keeps the raw URL, which the login below still needs.
|
|
4083
|
+
const shownServerUrl = logSafeOrigin(found.serverUrl);
|
|
4081
4084
|
// ACL enforcement: the target budget must be in this session's allowedBudgets.
|
|
4082
4085
|
// OIDC mode: explicit ACL required. #572: any mode whose request carries a
|
|
4083
4086
|
// restricting list (MCP_STATIC_TOKENS) is checked against it too; with no list
|
|
@@ -4129,7 +4132,7 @@ export async function switchBudget(name) {
|
|
|
4129
4132
|
setSessionBudget(sessionId, key);
|
|
4130
4133
|
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
4131
4134
|
logger.info(`[ADAPTER] switchBudget no-op for session ${sessionId}: already on "${found.name}" (${found.syncId})`);
|
|
4132
|
-
return { name: found.name, syncId: found.syncId, serverUrl:
|
|
4135
|
+
return { name: found.name, syncId: found.syncId, serverUrl: shownServerUrl };
|
|
4133
4136
|
}
|
|
4134
4137
|
if (sameAuth) {
|
|
4135
4138
|
// Same server + creds, different syncId. Reload budget file in place.
|
|
@@ -4150,8 +4153,8 @@ export async function switchBudget(name) {
|
|
|
4150
4153
|
connectionPool.updateLoadedSyncId(sessionId, found.syncId);
|
|
4151
4154
|
setSessionBudget(sessionId, key);
|
|
4152
4155
|
setPreferredBudgetSyncId(store?.principal, found.syncId); // #189: persist at commit
|
|
4153
|
-
logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${found.serverUrl}`);
|
|
4154
|
-
return { name: found.name, syncId: found.syncId, serverUrl:
|
|
4156
|
+
logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${logSafeOrigin(found.serverUrl)}`);
|
|
4157
|
+
return { name: found.name, syncId: found.syncId, serverUrl: shownServerUrl };
|
|
4155
4158
|
}
|
|
4156
4159
|
// #581: hold this session's slot across the window between releasing the old entry and
|
|
4157
4160
|
// creating the new one, so a concurrent initialize cannot take it and leave this session over
|
|
@@ -4220,8 +4223,8 @@ export async function switchBudget(name) {
|
|
|
4220
4223
|
if (holdsSlot)
|
|
4221
4224
|
connectionPool.releaseReservation(sessionId);
|
|
4222
4225
|
}
|
|
4223
|
-
logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${found.serverUrl}`);
|
|
4224
|
-
return { name: found.name, syncId: found.syncId, serverUrl:
|
|
4226
|
+
logger.info(`[ADAPTER] Active budget switched for session ${sessionId} to: "${found.name}" (${found.syncId}) on ${logSafeOrigin(found.serverUrl)}`);
|
|
4227
|
+
return { name: found.name, syncId: found.syncId, serverUrl: shownServerUrl };
|
|
4225
4228
|
}
|
|
4226
4229
|
/**
|
|
4227
4230
|
* Clear the per-session budget state for a session, so the map does not
|
|
@@ -4242,7 +4245,8 @@ export function getBudgetRegistry() {
|
|
|
4242
4245
|
return [...budgetRegistry.values()].filter(b => !allowed || allowed.includes(b.syncId)).map(b => ({
|
|
4243
4246
|
name: b.name,
|
|
4244
4247
|
syncId: b.syncId,
|
|
4245
|
-
|
|
4248
|
+
// #628: origin only; the registry entry itself keeps the raw URL for the login.
|
|
4249
|
+
serverUrl: logSafeOrigin(b.serverUrl),
|
|
4246
4250
|
hasEncryption: !!b.encryptionPassword,
|
|
4247
4251
|
}));
|
|
4248
4252
|
}
|
package/dist/src/lib/apiLogin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import api from '@actual-app/api';
|
|
2
|
+
import config from '../config.js';
|
|
2
3
|
import { withOpTimeout } from './opTimeout.js';
|
|
3
4
|
import { setApiLogin, setLoadedBudgetSyncId, registerBudgetLoad, loginsSpanServers } from './apiState.js';
|
|
4
5
|
import { createModuleLogger } from './loggerFactory.js';
|
|
@@ -38,6 +39,10 @@ let upstreamLib = null;
|
|
|
38
39
|
* entries are still valid.
|
|
39
40
|
* 3. log in, then record the login.
|
|
40
41
|
*
|
|
42
|
+
* Every init passes `verbose` explicitly (#620): upstream only sets its process-global verbose
|
|
43
|
+
* mode when the key is present, and its default is on, which writes reconciled transactions into
|
|
44
|
+
* our log through `wireConsoleAndDebug`.
|
|
45
|
+
*
|
|
41
46
|
* Bounded by the op timeout (#270). Callers that retry on the auth rate limit wrap this whole
|
|
42
47
|
* call in `withAuthRetry`, so each attempt is bounded on its own.
|
|
43
48
|
*
|
|
@@ -72,6 +77,7 @@ export async function initApiLoggedIn(dataDir, login, label) {
|
|
|
72
77
|
dataDir,
|
|
73
78
|
serverURL: login.serverUrl,
|
|
74
79
|
password: login.password,
|
|
80
|
+
verbose: config.ACTUAL_API_VERBOSE,
|
|
75
81
|
});
|
|
76
82
|
const lib = await withOpTimeout(() => loggingIn, label);
|
|
77
83
|
if (lib && typeof lib.send === 'function')
|
|
@@ -109,7 +115,7 @@ async function initOverFetchedLogin(dataDir, login, label) {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
// `password: ''` is upstream's no-credential init (it signs in only on a truthy password).
|
|
112
|
-
const pointing = api.init({ dataDir, serverURL: login.serverUrl, password: '' });
|
|
118
|
+
const pointing = api.init({ dataDir, serverURL: login.serverUrl, password: '', verbose: config.ACTUAL_API_VERBOSE });
|
|
113
119
|
registerBudgetLoad(pointing);
|
|
114
120
|
const lib = await withOpTimeout(() => pointing, label);
|
|
115
121
|
if (!lib || typeof lib.send !== 'function')
|
|
@@ -218,3 +218,20 @@ export function enforceHttpAuthPosture(input, log = logger, exit = process.exit)
|
|
|
218
218
|
}
|
|
219
219
|
return decision;
|
|
220
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* One word naming the HTTP auth mode for the startup banner (#626). Precedence
|
|
223
|
+
* mirrors the listen callback's posture lines in httpServer.ts: OIDC, then the
|
|
224
|
+
* static token map, then the single shared bearer token, then none. With both
|
|
225
|
+
* token variables set this reports `static`, as the ticket specifies; the
|
|
226
|
+
* listen callback still names the extra unrestricted token, and #623 replaces
|
|
227
|
+
* the banner. Never prints an issuer, token or principal.
|
|
228
|
+
*/
|
|
229
|
+
export function describeAuthMode(input) {
|
|
230
|
+
if (input.oidcEnabled)
|
|
231
|
+
return 'oidc';
|
|
232
|
+
if (input.hasStaticTokens)
|
|
233
|
+
return 'static';
|
|
234
|
+
if (input.hasSseAuthorization)
|
|
235
|
+
return 'bearer';
|
|
236
|
+
return 'none';
|
|
237
|
+
}
|
|
@@ -25,6 +25,44 @@
|
|
|
25
25
|
* The AI uses `actual_budgets_list_available` to see all configured budgets,
|
|
26
26
|
* then `actual_budgets_switch` with the budget name to switch between them.
|
|
27
27
|
*/
|
|
28
|
+
/**
|
|
29
|
+
* #621: a server URL as it may appear in a log line: scheme, host and port only. URL userinfo
|
|
30
|
+
* (a reverse-proxy credential), path and query are dropped. Never throws; an unparseable URL
|
|
31
|
+
* or one with a `null` origin (a non-special scheme such as a schemeless `user:pass@host`
|
|
32
|
+
* typo) becomes `<invalid url>`. Display text only, for log lines and tool results (#628):
|
|
33
|
+
* its output is never used to connect, and the login itself must keep the full URL.
|
|
34
|
+
*/
|
|
35
|
+
export function logSafeOrigin(url) {
|
|
36
|
+
try {
|
|
37
|
+
const origin = new URL(url).origin;
|
|
38
|
+
return origin === 'null' ? '<invalid url>' : origin;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return '<invalid url>';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* #621: the startup registry line, counts and schemes only, so no budget name, host or
|
|
46
|
+
* credential reaches the log. Servers are counted by the same [serverUrl, password] key
|
|
47
|
+
* setLoginsSpanServers uses, so the two can never disagree. Never throws.
|
|
48
|
+
*/
|
|
49
|
+
export function formatBudgetRegistrySummary(registry) {
|
|
50
|
+
const budgets = [...registry.values()];
|
|
51
|
+
const servers = new Set(budgets.map((b) => JSON.stringify([b.serverUrl, b.password || '']))).size;
|
|
52
|
+
const schemes = new Set();
|
|
53
|
+
for (const b of budgets) {
|
|
54
|
+
let scheme;
|
|
55
|
+
try {
|
|
56
|
+
const protocol = new URL(b.serverUrl).protocol;
|
|
57
|
+
scheme = protocol === 'http:' ? 'http' : protocol === 'https:' ? 'https' : 'other';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
scheme = 'invalid';
|
|
61
|
+
}
|
|
62
|
+
schemes.add(scheme);
|
|
63
|
+
}
|
|
64
|
+
return `Budget registry: ${budgets.length} budget(s) across ${servers} server(s) (${[...schemes].sort().join(', ')})`;
|
|
65
|
+
}
|
|
28
66
|
/**
|
|
29
67
|
* Parse the budget registry from environment variables.
|
|
30
68
|
* Always includes the default budget from the provided defaults (ACTUAL_* vars).
|
|
@@ -62,7 +100,7 @@ export function parseBudgetRegistry(env, defaults) {
|
|
|
62
100
|
// Mirror the default-budget check (#161): never send an E2E encryption
|
|
63
101
|
// password over an http:// upstream unless ALLOW_INSECURE_UPSTREAM is set.
|
|
64
102
|
if (encryptionPassword && env.ALLOW_INSECURE_UPSTREAM !== 'true' && /^http:\/\//i.test(serverUrl)) {
|
|
65
|
-
console.error(`[CONFIG] BUDGET_${i}_ENCRYPTION_PASSWORD is set but the upstream URL is http:// (${serverUrl}). ` +
|
|
103
|
+
console.error(`[CONFIG] BUDGET_${i}_ENCRYPTION_PASSWORD is set but the upstream URL is http:// (${logSafeOrigin(serverUrl)}). ` +
|
|
66
104
|
`Refusing to send the encryption password over plaintext (#161). ` +
|
|
67
105
|
`Use https:// or set ALLOW_INSECURE_UPSTREAM=true to override.`);
|
|
68
106
|
process.exit(1);
|
|
@@ -3,6 +3,12 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { findRootPackageJson } from './node-version-guard.js';
|
|
6
|
+
/**
|
|
7
|
+
* #624: the MCP initialize `serverInfo.name` on both transports. It equals the package.json
|
|
8
|
+
* name (tests/unit/server_identity.test.js pins that), so a client shows the server under the
|
|
9
|
+
* name it was installed as, never a description sentence.
|
|
10
|
+
*/
|
|
11
|
+
export const SERVER_NAME = 'actual-mcp-server';
|
|
6
12
|
/**
|
|
7
13
|
* #541: the single source of this server's own version, shared by the MCP initialize
|
|
8
14
|
* `serverInfo.version` (src/index.ts) and the `actual_server_info` tool.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #622: the process-level unhandledRejection and uncaughtException handlers, as a pure
|
|
3
|
+
* factory so unit tests can drive them without exiting. src/lib/processHandlersInstall.ts
|
|
4
|
+
* registers them on `process`.
|
|
5
|
+
*
|
|
6
|
+
* Records are built from gated fields only (logging convention v1, agreed with
|
|
7
|
+
* actual-sync): `{ type, code?, httpStatus?, category, safeMessage }`. The reason itself,
|
|
8
|
+
* its message, its stack and any upstream `.type` never reach a record or winston, because
|
|
9
|
+
* a rejection reason can carry a request object with an Authorization header, and upstream
|
|
10
|
+
* text can quote account or payee names (#559).
|
|
11
|
+
*
|
|
12
|
+
* INVARIANT: imports only node: builtins, ./errors.js and ./rejection-allowlist.js, never
|
|
13
|
+
* logger.ts or loggerFactory.ts. This module loads before MCP_STDIO_MODE is set, and the
|
|
14
|
+
* logger reads that variable when winston is built.
|
|
15
|
+
*/
|
|
16
|
+
import { errorLogFields } from './errors.js';
|
|
17
|
+
import { isKnownBenignRejection } from './rejection-allowlist.js';
|
|
18
|
+
/** Fixed sentence per category, used when the throw site set no log-safe text. */
|
|
19
|
+
const SAFE_MESSAGES = {
|
|
20
|
+
not_found: 'A requested item was not found.',
|
|
21
|
+
validation: 'A request was rejected as invalid.',
|
|
22
|
+
auth: 'An upstream request was not authorized.',
|
|
23
|
+
timeout: 'An operation timed out.',
|
|
24
|
+
conflict: 'An operation conflicted with existing state.',
|
|
25
|
+
rate_limited: 'An upstream service rate limited the request.',
|
|
26
|
+
upstream_unavailable: 'An upstream service was unavailable.',
|
|
27
|
+
cancelled: 'An operation was cancelled.',
|
|
28
|
+
internal: 'An internal error occurred.',
|
|
29
|
+
};
|
|
30
|
+
const UNAVAILABLE_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'EPIPE']);
|
|
31
|
+
/** Closed mapping, first match wins. Never reads message text. */
|
|
32
|
+
export function categorizeError({ errorClass, refusalKind, code, httpStatus: s }) {
|
|
33
|
+
if (refusalKind === 'not-found')
|
|
34
|
+
return 'not_found';
|
|
35
|
+
if (refusalKind === 'out-of-range')
|
|
36
|
+
return 'validation';
|
|
37
|
+
if (s === 401 || s === 403)
|
|
38
|
+
return 'auth';
|
|
39
|
+
if (s === 404)
|
|
40
|
+
return 'not_found';
|
|
41
|
+
if (s === 408 || s === 504 || code === 'ETIMEDOUT')
|
|
42
|
+
return 'timeout';
|
|
43
|
+
if (s === 409)
|
|
44
|
+
return 'conflict';
|
|
45
|
+
if (s === 429)
|
|
46
|
+
return 'rate_limited';
|
|
47
|
+
if (s === 400 || s === 422)
|
|
48
|
+
return 'validation';
|
|
49
|
+
if ((s !== undefined && s >= 500 && s <= 599) || (code !== undefined && UNAVAILABLE_CODES.has(code))) {
|
|
50
|
+
return 'upstream_unavailable';
|
|
51
|
+
}
|
|
52
|
+
// The Node AbortError class (its ABORT_ERR code fails the E-shape gate). A fetch abort is a
|
|
53
|
+
// DOMException and falls to internal by design: this keys on the class, never on `name`.
|
|
54
|
+
if (errorClass === 'AbortError')
|
|
55
|
+
return 'cancelled';
|
|
56
|
+
return 'internal';
|
|
57
|
+
}
|
|
58
|
+
function isHttpStatus(v) {
|
|
59
|
+
return typeof v === 'number' && Number.isInteger(v) && v >= 100 && v <= 599;
|
|
60
|
+
}
|
|
61
|
+
function readHttpStatus(reason) {
|
|
62
|
+
if (typeof reason !== 'object' || reason === null)
|
|
63
|
+
return undefined;
|
|
64
|
+
const r = reason;
|
|
65
|
+
if (isHttpStatus(r.status))
|
|
66
|
+
return r.status;
|
|
67
|
+
if (isHttpStatus(r.statusCode))
|
|
68
|
+
return r.statusCode;
|
|
69
|
+
const response = r.response;
|
|
70
|
+
if (typeof response === 'object' && response !== null && isHttpStatus(response.status))
|
|
71
|
+
return response.status;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The gated error object of a record. `type` is the prototype class name, never the
|
|
76
|
+
* reason's own `.type` property (that can carry CR/LF from upstream). Can throw when the
|
|
77
|
+
* reason has a hostile getter; the handlers catch that and write the fixed fallback.
|
|
78
|
+
*/
|
|
79
|
+
export function errorRecordFields(reason) {
|
|
80
|
+
const { errorClass, refusalKind, code, logMessage } = errorLogFields(reason);
|
|
81
|
+
const httpStatus = readHttpStatus(reason);
|
|
82
|
+
const category = categorizeError({ errorClass, refusalKind, code, httpStatus });
|
|
83
|
+
const record = { type: errorClass, category, safeMessage: logMessage ?? SAFE_MESSAGES[category] };
|
|
84
|
+
if (code !== undefined)
|
|
85
|
+
record.code = code;
|
|
86
|
+
if (httpStatus !== undefined)
|
|
87
|
+
record.httpStatus = httpStatus;
|
|
88
|
+
return record;
|
|
89
|
+
}
|
|
90
|
+
const NODE_FLOOR_HINT = [
|
|
91
|
+
`This almost certainly means Node ${process.version} is too old.`,
|
|
92
|
+
'actual-mcp-server requires the Node version in package.json "engines".',
|
|
93
|
+
'Import attributes (`with { type: "json" }`) need Node 22 or newer.',
|
|
94
|
+
];
|
|
95
|
+
/**
|
|
96
|
+
* Upstream raises bank sync failures as plain Errors with no code, so message text is the
|
|
97
|
+
* only signal. This is a documented exception to the no-text-matching rule: the text only
|
|
98
|
+
* decides survive versus exit, and is never logged.
|
|
99
|
+
*/
|
|
100
|
+
function isBankSyncError(error) {
|
|
101
|
+
const errMsg = String(error?.message || error);
|
|
102
|
+
const errType = error?.type;
|
|
103
|
+
return (errType === 'BankSyncError' ||
|
|
104
|
+
errMsg.includes('BankSyncError') ||
|
|
105
|
+
errMsg.includes('NORDIGEN_ERROR') ||
|
|
106
|
+
errMsg.includes('RATE_LIMIT_EXCEEDED') ||
|
|
107
|
+
errMsg.includes('Rate limit exceeded') ||
|
|
108
|
+
errMsg.includes('Failed syncing account') ||
|
|
109
|
+
errMsg.includes('GoCardless') ||
|
|
110
|
+
errMsg.includes('SimpleFIN'));
|
|
111
|
+
}
|
|
112
|
+
export function createProcessHandlers(deps) {
|
|
113
|
+
const { exit, writeSync, getLogger } = deps;
|
|
114
|
+
// Fatal: the one synchronous record, then exit 1 even when the write fails. Never also
|
|
115
|
+
// through winston, which under stdio already writes to stderr (one record per event).
|
|
116
|
+
function crash(source, error) {
|
|
117
|
+
try {
|
|
118
|
+
writeSync(2, JSON.stringify({ level: 'error', event: 'sys_crash', source, error }) + '\n');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// stderr is a full or closed pipe: nothing more can be written.
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function fallback(source) {
|
|
128
|
+
crash(source, { type: 'unknown', category: 'internal', safeMessage: SAFE_MESSAGES.internal });
|
|
129
|
+
}
|
|
130
|
+
function survive(source, event, message, error) {
|
|
131
|
+
const logger = getLogger();
|
|
132
|
+
if (logger) {
|
|
133
|
+
logger.warn(message, { event, source, error });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
writeSync(2, JSON.stringify({ level: 'warn', event, source, error }) + '\n');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function writeNodeFloorHint(error) {
|
|
140
|
+
if (error.code !== 'ERR_IMPORT_ASSERTION_TYPE_MISSING')
|
|
141
|
+
return;
|
|
142
|
+
for (const line of NODE_FLOOR_HINT)
|
|
143
|
+
writeSync(2, line + '\n');
|
|
144
|
+
}
|
|
145
|
+
function onUnhandledRejection(reason) {
|
|
146
|
+
try {
|
|
147
|
+
const error = errorRecordFields(reason);
|
|
148
|
+
// #275 backstop: a library consumer reaches neither entry point's guard, and import
|
|
149
|
+
// attributes fail asynchronously on an under-floor Node, so name the real cause.
|
|
150
|
+
writeNodeFloorHint(error);
|
|
151
|
+
if (isKnownBenignRejection(reason)) {
|
|
152
|
+
survive('unhandledRejection', 'process.rejection_benign', 'Process-level rejection survived', error);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
crash('unhandledRejection', error);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
fallback('unhandledRejection');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function onUncaughtException(reason) {
|
|
162
|
+
try {
|
|
163
|
+
const error = errorRecordFields(reason);
|
|
164
|
+
writeNodeFloorHint(error);
|
|
165
|
+
// GoCardless and SimpleFIN failures can surface here from inside @actual-app/api when
|
|
166
|
+
// the provider answers asynchronously (RATE_LIMIT_EXCEEDED, for example). Non-fatal.
|
|
167
|
+
if (isBankSyncError(reason)) {
|
|
168
|
+
if (error.category === 'internal') {
|
|
169
|
+
error.category = 'upstream_unavailable';
|
|
170
|
+
if (error.safeMessage === SAFE_MESSAGES.internal)
|
|
171
|
+
error.safeMessage = SAFE_MESSAGES.upstream_unavailable;
|
|
172
|
+
}
|
|
173
|
+
survive('uncaughtException', 'bank_sync.exception_benign', 'Bank sync error survived', error);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
crash('uncaughtException', error);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
fallback('uncaughtException');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { onUnhandledRejection, onUncaughtException };
|
|
183
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #622: side-effect module that registers the process-level handlers. src/index.ts imports
|
|
3
|
+
* it SECOND, right after ./node-version-guard.js (#275), so the handlers are armed before
|
|
4
|
+
* any other module evaluates. The logger arrives later through attachProcessLogger, once
|
|
5
|
+
* src/index.ts has set MCP_STDIO_MODE and loaded src/logger.ts; until then the handlers
|
|
6
|
+
* write one fixed JSON line to stderr.
|
|
7
|
+
*
|
|
8
|
+
* INVARIANT: no import of logger.ts or loggerFactory.ts (see processHandlers.ts).
|
|
9
|
+
*/
|
|
10
|
+
import { writeSync } from 'node:fs';
|
|
11
|
+
import { createProcessHandlers } from './processHandlers.js';
|
|
12
|
+
let attached;
|
|
13
|
+
/** Route the survivable records through the logger from now on. */
|
|
14
|
+
export function attachProcessLogger(logger) {
|
|
15
|
+
attached = logger;
|
|
16
|
+
}
|
|
17
|
+
const handlers = createProcessHandlers({
|
|
18
|
+
exit: (code) => process.exit(code),
|
|
19
|
+
writeSync: (fd, text) => {
|
|
20
|
+
writeSync(fd, text);
|
|
21
|
+
},
|
|
22
|
+
getLogger: () => attached,
|
|
23
|
+
});
|
|
24
|
+
process.on('unhandledRejection', handlers.onUnhandledRejection);
|
|
25
|
+
process.on('uncaughtException', handlers.onUncaughtException);
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* INVARIANT: this module MUST remain side-effect-free.
|
|
9
9
|
*
|
|
10
|
-
* src/
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* src/lib/processHandlersInstall.ts (the second import of src/index.ts, #622)
|
|
11
|
+
* registers the process.on('unhandledRejection', ...) handler AFTER importing
|
|
12
|
+
* this module through processHandlers.ts (ESM static imports are evaluated
|
|
13
|
+
* before the top-level body runs). Any side effect introduced here, e.g. importing a
|
|
13
14
|
* logger that performs winston/file-handle init, importing dotenv, or any
|
|
14
15
|
* code that runs at module load, opens a window during ESM evaluation in
|
|
15
16
|
* which an unhandled rejection bypasses this allow-list and crashes the
|
|
@@ -5,6 +5,8 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
5
5
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import logger, { resolveRequestId } from '../logger.js';
|
|
7
7
|
import { attachTransportErrorLogger } from '../lib/transportErrorLogger.js';
|
|
8
|
+
import { SERVER_NAME } from '../lib/mcp-version.js';
|
|
9
|
+
import { createModuleLogger } from '../lib/loggerFactory.js';
|
|
8
10
|
import { getLocalIp } from '../utils.js';
|
|
9
11
|
import actualToolsManager from '../actualToolsManager.js';
|
|
10
12
|
import { getConnectionState, connectToActualForSession, shutdownActualForSession, shutdownActual, admitSession, releaseAdmission, isConnectionPoolEnabled } from '../actualConnection.js';
|
|
@@ -43,6 +45,7 @@ import { buildToolListEntries } from '../lib/tool-list-entry.js';
|
|
|
43
45
|
import { classifyInitFailure } from '../lib/init-failure.js';
|
|
44
46
|
import { createHttpErrorHandler } from './httpErrorHandler.js';
|
|
45
47
|
export { classifyInitFailure };
|
|
48
|
+
const httpLog = createModuleLogger('HTTP');
|
|
46
49
|
export { requestContext };
|
|
47
50
|
// Resolve the authenticated principal for the per-principal budget preference
|
|
48
51
|
// (#189). OIDC: the verified JWT subject. A MCP_STATIC_TOKENS token (#572): its
|
|
@@ -486,7 +489,7 @@ advertisedUrl) {
|
|
|
486
489
|
capabilities: capabilitiesObj,
|
|
487
490
|
};
|
|
488
491
|
const server = new Server({
|
|
489
|
-
name:
|
|
492
|
+
name: SERVER_NAME,
|
|
490
493
|
version: version || "0.1.0",
|
|
491
494
|
}, serverOptions);
|
|
492
495
|
attachTransportErrorLogger(server, 'http');
|
|
@@ -1124,9 +1127,9 @@ advertisedUrl) {
|
|
|
1124
1127
|
// #239: when bound to a specific (non all-interfaces) host the server is NOT on
|
|
1125
1128
|
// loopback, so point the health hint at the actual bind host instead of localhost.
|
|
1126
1129
|
const healthHost = (!bindHost || bindHost === '0.0.0.0' || bindHost === '::') ? 'localhost' : bindHost;
|
|
1127
|
-
|
|
1128
|
-
console.info
|
|
1129
|
-
|
|
1130
|
+
const health = `${scheme}://${healthHost}:${port}/health`;
|
|
1131
|
+
// #625: one structured record through the module logger instead of three console.info lines.
|
|
1132
|
+
httpLog.info(`MCP Streamable HTTP Server listening on ${port}; MCP endpoint: ${advertised} (health: ${health})`, { port, endpoint: advertised, health });
|
|
1130
1133
|
if (config.AUTH_PROVIDER === 'oidc') {
|
|
1131
1134
|
logger.info(`🔒 OIDC authentication enabled (JWT Bearer token required; issuer: ${config.OIDC_ISSUER})`);
|
|
1132
1135
|
// #345: name the userNames the ACL can match, once, at boot. Deliberately
|
|
@@ -5,6 +5,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprot
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import logger from '../logger.js';
|
|
7
7
|
import { attachTransportErrorLogger } from '../lib/transportErrorLogger.js';
|
|
8
|
+
import { SERVER_NAME } from '../lib/mcp-version.js';
|
|
8
9
|
import actualToolsManager from '../actualToolsManager.js';
|
|
9
10
|
import { requestContext } from '../lib/requestContext.js';
|
|
10
11
|
import { buildToolListEntries } from '../lib/tool-list-entry.js';
|
|
@@ -85,7 +86,9 @@ export function createStdioCallToolHandler(mcp, stdioSessionId) {
|
|
|
85
86
|
}
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
|
-
export async function startStdioServer(mcp, capabilities, implementedTools,
|
|
89
|
+
export async function startStdioServer(mcp, capabilities, implementedTools,
|
|
90
|
+
// #624: no longer the serverInfo name; kept so the positional signature matches HTTP.
|
|
91
|
+
_serverDescription, instructions, toolSchemas, version) {
|
|
89
92
|
const toolsList = Array.isArray(implementedTools) ? implementedTools : [];
|
|
90
93
|
// #348: a synthetic session identity for this stdio process.
|
|
91
94
|
//
|
|
@@ -108,7 +111,7 @@ export async function startStdioServer(mcp, capabilities, implementedTools, serv
|
|
|
108
111
|
// restarts and can never collide with an HTTP session id.
|
|
109
112
|
const stdioSessionId = `stdio-${randomUUID()}`;
|
|
110
113
|
logger.debug(`[STDIO] session id ${stdioSessionId}`);
|
|
111
|
-
const server = new Server({ name:
|
|
114
|
+
const server = new Server({ name: SERVER_NAME, version: version || '0.1.0' }, { capabilities, instructions });
|
|
112
115
|
attachTransportErrorLogger(server, 'stdio');
|
|
113
116
|
// List tools handler — mirrors createServerInstance() in httpServer.ts
|
|
114
117
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -4,7 +4,7 @@ const InputSchema = z.object({});
|
|
|
4
4
|
const tool = {
|
|
5
5
|
name: 'actual_budgets_list_available',
|
|
6
6
|
description: 'List the pre-configured budgets this session may switch to (a session limited to some budgets sees only those). ' +
|
|
7
|
-
'Each entry shows the budget name, sync ID, server
|
|
7
|
+
'Each entry shows the budget name, sync ID, server origin, and whether it uses E2E encryption. ' +
|
|
8
8
|
'Budgets are configured via BUDGET_n_NAME / BUDGET_n_SYNC_ID / BUDGET_n_SERVER_URL env vars. ' +
|
|
9
9
|
'Pass the budget name to actual_budgets_switch to change the active budget.',
|
|
10
10
|
inputSchema: InputSchema,
|
|
@@ -3,7 +3,7 @@ import adapter from '../lib/actual-adapter.js';
|
|
|
3
3
|
import { CommonSchemas } from '../lib/schemas/common.js';
|
|
4
4
|
import { RecurConfigSchema } from '../lib/schemas/recur.js';
|
|
5
5
|
import { ScheduleAmountSchema, amountOpMismatch } from '../lib/schemas/schedule-amount.js';
|
|
6
|
-
const InputSchema = z.
|
|
6
|
+
const InputSchema = z.strictObject({
|
|
7
7
|
id: CommonSchemas.scheduleId.describe('Schedule UUID'),
|
|
8
8
|
name: z.string().optional()
|
|
9
9
|
.describe('New display name'),
|
|
@@ -24,10 +24,12 @@ const InputSchema = z.object({
|
|
|
24
24
|
.describe('New YYYY-MM-DD date or RecurConfig object'),
|
|
25
25
|
posts_transaction: z.boolean().optional()
|
|
26
26
|
.describe('Auto-post a transaction on each occurrence'),
|
|
27
|
-
completed: z.boolean().optional()
|
|
28
|
-
.describe('true marks it completed, false reactivates it'),
|
|
29
27
|
resetNextDate: z.boolean().optional().default(false)
|
|
30
28
|
.describe('Recalculate next_date'),
|
|
29
|
+
// #627: strict, because upstream updateSchedule refuses `completed` and `next_date` as
|
|
30
|
+
// system-managed. A stripped key would return success while changing nothing; an advertised
|
|
31
|
+
// one failed upstream after the write queue. Either is refused here, before any write, as
|
|
32
|
+
// "unexpected field(s): completed" (formatZodError sanitises the key); the description says why.
|
|
31
33
|
}).superRefine((input, ctx) => {
|
|
32
34
|
// #527: with both fields supplied the input alone decides. With only one, the answer
|
|
33
35
|
// depends on the stored schedule, so adapter.updateSchedule checks it inside the write queue.
|
|
@@ -39,7 +41,7 @@ const InputSchema = z.object({
|
|
|
39
41
|
});
|
|
40
42
|
const tool = {
|
|
41
43
|
name: 'actual_schedules_update',
|
|
42
|
-
description: `Update a schedule; supply only the fields to change. A new date replaces the stored recurrence, patterns included. Set resetNextDate: true when changing the date or recurrence.`,
|
|
44
|
+
description: `Update a schedule; supply only the fields to change. A new date replaces the stored recurrence, patterns included. Set resetNextDate: true when changing the date or recurrence. Completion and next_date are managed by Actual and cannot be set.`,
|
|
43
45
|
inputSchema: InputSchema,
|
|
44
46
|
call: async (args, _meta) => {
|
|
45
47
|
const input = InputSchema.parse(args || {});
|
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.26.
|
|
4
|
+
"version": "0.26.1",
|
|
5
5
|
"mcpName": "io.github.agigante80/actual-mcp-server",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0",
|
|
@@ -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/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",
|
|
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/api_login_verbose_optin.test.js && node tests/unit/config_actual_api_verbose.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/server_no_console.test.js && node tests/unit/auth_mode.test.js && node tests/unit/process_handlers.test.js && node tests/unit/process_handlers_child.test.js && node tests/unit/server_identity.test.js && node tests/unit/budgets_serverurl_redaction.test.js && node tests/unit/budgets_serverurl_redaction_default.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",
|