actual-mcp-server 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/actualToolsManager.js +4 -0
- package/dist/src/index.js +54 -24
- package/dist/src/lib/actual-adapter.js +5 -1
- package/dist/src/lib/budget-registry.js +10 -5
- package/dist/src/lib/startupRecord.js +141 -0
- package/dist/src/server/httpServer.js +28 -7
- package/dist/src/tools/server_info.js +15 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -195,6 +195,10 @@ class ActualToolsManager {
|
|
|
195
195
|
const { toolsets, tools, readOnly } = this.requirePolicy().settings;
|
|
196
196
|
return { toolsets: [...toolsets], tools: [...tools], readOnly };
|
|
197
197
|
}
|
|
198
|
+
/** How many tools exist, published or not, for the sys_startup record (#623). */
|
|
199
|
+
getRegisteredToolCount() {
|
|
200
|
+
return this.tools.size;
|
|
201
|
+
}
|
|
198
202
|
getToolNames() {
|
|
199
203
|
return Array.from(this.tools.keys());
|
|
200
204
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -147,7 +147,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
147
147
|
import('./actualConnection.js'),
|
|
148
148
|
import('./lib/ActualMCPConnection.js'),
|
|
149
149
|
]);
|
|
150
|
-
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture, findWeakTokenVars
|
|
150
|
+
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture, findWeakTokenVars }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
151
151
|
import('./server/httpServer.js'),
|
|
152
152
|
import('./server/stdioServer.js'),
|
|
153
153
|
import('./lib/authPosture.js'),
|
|
@@ -259,6 +259,8 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
259
259
|
// the configured posture is safe. On a non-loopback bind with no auth and no
|
|
260
260
|
// explicit opt-out this refuses startup (exit 1) rather than silently serving
|
|
261
261
|
// financial data open on the LAN. stdio mode never reaches this check.
|
|
262
|
+
// #623: kept for the sys_startup record; 'n/a' on stdio, which has no posture.
|
|
263
|
+
let posture = 'n/a';
|
|
262
264
|
if (useHttp || useTestMcpClient) {
|
|
263
265
|
const oidcEnabled = process.env.AUTH_PROVIDER === 'oidc';
|
|
264
266
|
const decision = enforceHttpAuthPosture({
|
|
@@ -276,6 +278,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
276
278
|
// if process.exit were ever non-terminal, so we never bind on a refuse.
|
|
277
279
|
if (decision === 'refuse')
|
|
278
280
|
return;
|
|
281
|
+
posture = decision;
|
|
279
282
|
}
|
|
280
283
|
// If requested, run the MCP client-side tests now that all variables are ready
|
|
281
284
|
if (useTestMcpClient) {
|
|
@@ -313,38 +316,65 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
313
316
|
process.exit(1);
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
319
|
+
// #623: the one structured startup record, logged only once the transport is really up
|
|
320
|
+
// (never on a posture refusal, a port clash or --test-mcp-client). It replaces the old
|
|
321
|
+
// banner, which printed every tool name and the instructions text. Failure isolated: a
|
|
322
|
+
// throw here logs one warn line with the error name and never stops a started server.
|
|
323
|
+
const emitStartupRecord = async (transport) => {
|
|
324
|
+
try {
|
|
325
|
+
const [{ buildStartupRecord }, { resolveLogConfig }, { default: config }, { connectionPool }, { getBudgetRegistrySummary }, { existsSync }] = await Promise.all([
|
|
326
|
+
import('./lib/startupRecord.js'),
|
|
327
|
+
import('./logger.js'),
|
|
328
|
+
import('./config.js'),
|
|
329
|
+
import('./lib/ActualConnectionPool.js'),
|
|
330
|
+
import('./lib/actual-adapter.js'),
|
|
331
|
+
import('node:fs'),
|
|
332
|
+
]);
|
|
333
|
+
const policy = actualToolsManager.getToolPolicy();
|
|
334
|
+
const record = buildStartupRecord({
|
|
335
|
+
env: process.env,
|
|
336
|
+
logConfig: resolveLogConfig(process.env),
|
|
337
|
+
budgets: getBudgetRegistrySummary(),
|
|
338
|
+
tools: {
|
|
339
|
+
published: actualToolsManager.getPublishedToolNames().length,
|
|
340
|
+
registered: actualToolsManager.getRegisteredToolCount(),
|
|
341
|
+
policy: { toolsets: policy.toolsets, readOnly: policy.readOnly },
|
|
342
|
+
},
|
|
343
|
+
posture,
|
|
344
|
+
actualApiVerbose: config.ACTUAL_API_VERBOSE,
|
|
345
|
+
// From the pool, not config: an empty config value parses to NaN (#623 gate).
|
|
346
|
+
limits: {
|
|
347
|
+
opTimeoutMs: config.ACTUAL_OP_TIMEOUT_MS,
|
|
348
|
+
maxSessions: connectionPool.getStats().maxConcurrent,
|
|
349
|
+
idleTimeoutMs: connectionPool.getIdleTimeoutMinutes() * 60000,
|
|
350
|
+
},
|
|
351
|
+
runtime: {
|
|
352
|
+
container: existsSync('/.dockerenv') || !!process.env.container,
|
|
353
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
354
|
+
utcOffsetMinutes: -new Date().getTimezoneOffset(),
|
|
355
|
+
},
|
|
356
|
+
transport,
|
|
357
|
+
});
|
|
358
|
+
logger.info('sys_startup', record);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
logger.warn('sys_startup record failed', { error: err instanceof Error ? err.name : 'unknown' });
|
|
362
|
+
}
|
|
363
|
+
};
|
|
337
364
|
if (useHttp) {
|
|
338
|
-
|
|
365
|
+
// The listen callback is synchronous; the record is fire-and-forget from there.
|
|
339
366
|
await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, serverInstructionsText, toolSchemas, version,
|
|
340
367
|
// bind host (ensure server accepts connections on that interface)
|
|
341
368
|
bindHost,
|
|
342
369
|
// advertised URL shown to clients
|
|
343
|
-
advertisedUrl)
|
|
370
|
+
advertisedUrl, (listener) => {
|
|
371
|
+
void emitStartupRecord({ kind: 'http', bindHost, path: HTTP_PATH, advertisedUrl, listener });
|
|
372
|
+
});
|
|
344
373
|
}
|
|
345
374
|
else if (useStdio) {
|
|
346
375
|
logger.debug('Mode: stdio');
|
|
347
376
|
await startStdioServer(mcp, capabilities, implementedTools, SERVER_DESCRIPTION, serverInstructionsText, toolSchemas, version);
|
|
377
|
+
await emitStartupRecord({ kind: 'stdio' });
|
|
348
378
|
}
|
|
349
379
|
else {
|
|
350
380
|
logger.error('❌ Please specify a transport mode: --http or --stdio');
|
|
@@ -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 { formatBudgetRegistrySummary, logSafeOrigin, parseBudgetRegistry } from './budget-registry.js';
|
|
22
|
+
import { formatBudgetRegistrySummary, logSafeOrigin, parseBudgetRegistry, summarizeBudgetRegistry } 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';
|
|
@@ -40,6 +40,10 @@ const budgetRegistry = parseBudgetRegistry(process.env, {
|
|
|
40
40
|
setLoginsSpanServers(new Set([...budgetRegistry.values()].map((b) => JSON.stringify([b.serverUrl, b.password || '']))).size > 1);
|
|
41
41
|
// #621: counts and schemes only; budget names and server hosts stay out of the startup log.
|
|
42
42
|
logger.info(`[ADAPTER] ${formatBudgetRegistrySummary(budgetRegistry)}`);
|
|
43
|
+
/** #623: the registry's counts and schemes for the startup record. Reads config only, never Actual. */
|
|
44
|
+
export function getBudgetRegistrySummary() {
|
|
45
|
+
return summarizeBudgetRegistry(budgetRegistry);
|
|
46
|
+
}
|
|
43
47
|
/**
|
|
44
48
|
* Per-session active budget. Map from sessionId to lowercased budget key
|
|
45
49
|
* (matches the keys in budgetRegistry).
|
|
@@ -42,13 +42,13 @@ export function logSafeOrigin(url) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* #621
|
|
46
|
-
*
|
|
45
|
+
* #621 / #623: counts and schemes only, from PARSED URLs, so no budget name, host or credential
|
|
46
|
+
* can leave through it. Servers are counted by the same [serverUrl, password] key
|
|
47
47
|
* setLoginsSpanServers uses, so the two can never disagree. Never throws.
|
|
48
48
|
*/
|
|
49
|
-
export function
|
|
49
|
+
export function summarizeBudgetRegistry(registry) {
|
|
50
50
|
const budgets = [...registry.values()];
|
|
51
|
-
const
|
|
51
|
+
const distinctServers = new Set(budgets.map((b) => JSON.stringify([b.serverUrl, b.password || '']))).size;
|
|
52
52
|
const schemes = new Set();
|
|
53
53
|
for (const b of budgets) {
|
|
54
54
|
let scheme;
|
|
@@ -61,7 +61,12 @@ export function formatBudgetRegistrySummary(registry) {
|
|
|
61
61
|
}
|
|
62
62
|
schemes.add(scheme);
|
|
63
63
|
}
|
|
64
|
-
return
|
|
64
|
+
return { count: budgets.length, distinctServers, schemes: [...schemes].sort() };
|
|
65
|
+
}
|
|
66
|
+
/** #621: the startup registry line, built from summarizeBudgetRegistry. Never throws. */
|
|
67
|
+
export function formatBudgetRegistrySummary(registry) {
|
|
68
|
+
const { count, distinctServers, schemes } = summarizeBudgetRegistry(registry);
|
|
69
|
+
return `Budget registry: ${count} budget(s) across ${distinctServers} server(s) (${schemes.join(', ')})`;
|
|
65
70
|
}
|
|
66
71
|
/**
|
|
67
72
|
* Parse the budget registry from environment variables.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// #623: the one structured startup record, `sys_startup`, logged at info on both transports
|
|
2
|
+
// after the server has really started. Built by a pure allowlist function: every input is
|
|
3
|
+
// injected, so it reads no env, no config and no Actual, and a field that is not listed in
|
|
4
|
+
// STARTUP_RECORD_KEYS cannot appear. It imports neither src/index.ts, src/server/* nor the tool
|
|
5
|
+
// registry (src/tools/server_info.ts imports this file, so importing the registry would cycle).
|
|
6
|
+
import { INSTALLED_API_VERSION, resolveInstalledVersion } from './installed-api-version.js';
|
|
7
|
+
import { getMcpServerVersion } from './mcp-version.js';
|
|
8
|
+
import { describeAuthMode, isLoopbackHost } from './authPosture.js';
|
|
9
|
+
import { logSafeOrigin } from './budget-registry.js';
|
|
10
|
+
import { parseStaticTokensConfig, staticTokensConfigured } from '../auth/static-tokens.js';
|
|
11
|
+
/** Resolved once at module load: the walk does blocking file reads. The SDK exports no root
|
|
12
|
+
* path, so the resolvable subpath is passed explicitly (#445). */
|
|
13
|
+
const MCP_SDK_RESOLVED = resolveInstalledVersion('@modelcontextprotocol/sdk', '@modelcontextprotocol/sdk/server/index.js');
|
|
14
|
+
/** Every key the record may hold, by group. `event` is the one top-level scalar. */
|
|
15
|
+
export const STARTUP_RECORD_KEYS = {
|
|
16
|
+
event: [],
|
|
17
|
+
versions: ['server', 'node', 'actualApi', 'mcpSdk'],
|
|
18
|
+
runtime: ['platform', 'arch', 'container', 'nodeEnv', 'timezone', 'utcOffsetMinutes'],
|
|
19
|
+
logging: ['level', 'format', 'fileSink'],
|
|
20
|
+
privacy: ['actualApiVerbose'],
|
|
21
|
+
paths: ['logDirCustom', 'dataDirCustom'],
|
|
22
|
+
transport: ['kind', 'bindScope', 'port', 'path', 'scheme', 'advertisedOrigin'],
|
|
23
|
+
auth: ['mode', 'posture', 'sharedBearerAccepted', 'issuerOrigin', 'staticPrincipals'],
|
|
24
|
+
tools: ['total', 'registered', 'readOnly', 'toolsets'],
|
|
25
|
+
budgets: ['count', 'distinctServers', 'schemes'],
|
|
26
|
+
limits: ['opTimeoutMs', 'maxSessions', 'idleTimeoutMs', 'keepAliveMs'],
|
|
27
|
+
};
|
|
28
|
+
/** Keys present only on the HTTP transport, as `group.key`. */
|
|
29
|
+
export const STARTUP_RECORD_HTTP_ONLY_KEYS = [
|
|
30
|
+
'transport.bindScope', 'transport.port', 'transport.path', 'transport.scheme',
|
|
31
|
+
'transport.advertisedOrigin', 'limits.keepAliveMs',
|
|
32
|
+
];
|
|
33
|
+
/** Keys present only under AUTH_PROVIDER=oidc, as `group.key`. */
|
|
34
|
+
export const STARTUP_RECORD_OIDC_ONLY_KEYS = ['auth.issuerOrigin'];
|
|
35
|
+
/** Shared with actual_server_info, so the tool and the record can never report different
|
|
36
|
+
* resolved versions or tool counts. */
|
|
37
|
+
export function collectVersionAndToolFacts(counts) {
|
|
38
|
+
return {
|
|
39
|
+
versions: {
|
|
40
|
+
server: getMcpServerVersion(),
|
|
41
|
+
node: process.version,
|
|
42
|
+
actualApi: INSTALLED_API_VERSION,
|
|
43
|
+
mcpSdk: MCP_SDK_RESOLVED,
|
|
44
|
+
},
|
|
45
|
+
tools: {
|
|
46
|
+
total: counts.published,
|
|
47
|
+
registered: counts.registered,
|
|
48
|
+
readOnly: counts.policy.readOnly,
|
|
49
|
+
toolsets: [...counts.policy.toolsets],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function bindScopeOf(host) {
|
|
54
|
+
if (isLoopbackHost(host))
|
|
55
|
+
return 'loopback';
|
|
56
|
+
if (!host || host === '0.0.0.0' || host === '::')
|
|
57
|
+
return 'all-interfaces';
|
|
58
|
+
return 'specific';
|
|
59
|
+
}
|
|
60
|
+
/** A count of the configured static principals. config.ts already refused a malformed value at
|
|
61
|
+
* startup, so a throw here only means "not countable": 0, never a crash. */
|
|
62
|
+
function countStaticPrincipals(raw) {
|
|
63
|
+
try {
|
|
64
|
+
return Object.keys(parseStaticTokensConfig(raw)).length;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the `sys_startup` metadata. Origins go through logSafeOrigin (never throws, drops
|
|
72
|
+
* userinfo, path and query); budget facts arrive as counts; no hostname, bind address, path,
|
|
73
|
+
* budget or principal name, tool name or instructions text is ever read into it.
|
|
74
|
+
*/
|
|
75
|
+
export function buildStartupRecord(inputs) {
|
|
76
|
+
const { env, transport } = inputs;
|
|
77
|
+
const oidcEnabled = env.AUTH_PROVIDER === 'oidc';
|
|
78
|
+
const facts = collectVersionAndToolFacts(inputs.tools);
|
|
79
|
+
const http = transport.kind === 'http' ? transport : undefined;
|
|
80
|
+
return {
|
|
81
|
+
event: 'sys_startup',
|
|
82
|
+
versions: facts.versions,
|
|
83
|
+
runtime: {
|
|
84
|
+
platform: process.platform,
|
|
85
|
+
arch: process.arch,
|
|
86
|
+
container: inputs.runtime.container,
|
|
87
|
+
nodeEnv: env.NODE_ENV ?? null,
|
|
88
|
+
timezone: inputs.runtime.timezone,
|
|
89
|
+
utcOffsetMinutes: inputs.runtime.utcOffsetMinutes,
|
|
90
|
+
},
|
|
91
|
+
logging: {
|
|
92
|
+
level: inputs.logConfig.level,
|
|
93
|
+
format: inputs.logConfig.useJson ? 'json' : 'pretty',
|
|
94
|
+
// The same rule as STORE_LOGS in src/logger.ts.
|
|
95
|
+
fileSink: env.MCP_BRIDGE_STORE_LOGS === 'true',
|
|
96
|
+
},
|
|
97
|
+
privacy: {
|
|
98
|
+
actualApiVerbose: inputs.actualApiVerbose,
|
|
99
|
+
},
|
|
100
|
+
paths: {
|
|
101
|
+
logDirCustom: !!env.MCP_BRIDGE_LOG_DIR,
|
|
102
|
+
dataDirCustom: !!env.MCP_BRIDGE_DATA_DIR,
|
|
103
|
+
},
|
|
104
|
+
transport: http
|
|
105
|
+
? {
|
|
106
|
+
kind: 'http',
|
|
107
|
+
bindScope: bindScopeOf(http.bindHost),
|
|
108
|
+
port: http.listener.port,
|
|
109
|
+
path: http.path,
|
|
110
|
+
scheme: http.listener.scheme,
|
|
111
|
+
advertisedOrigin: logSafeOrigin(http.advertisedUrl),
|
|
112
|
+
}
|
|
113
|
+
: { kind: 'stdio' },
|
|
114
|
+
auth: {
|
|
115
|
+
// #626: the same blank-means-off rules as the posture check. MCP_SSE_AUTHORIZATION stays
|
|
116
|
+
// untrimmed: a whitespace-only value is still a live credential there.
|
|
117
|
+
mode: describeAuthMode({
|
|
118
|
+
oidcEnabled: env.AUTH_PROVIDER === 'oidc',
|
|
119
|
+
hasStaticTokens: staticTokensConfigured(env.MCP_STATIC_TOKENS),
|
|
120
|
+
hasSseAuthorization: !!env.MCP_SSE_AUTHORIZATION,
|
|
121
|
+
}),
|
|
122
|
+
posture: inputs.posture,
|
|
123
|
+
// Under OIDC the bearer entries are never built (httpServer.ts), so the token is not accepted.
|
|
124
|
+
sharedBearerAccepted: !oidcEnabled && !!env.MCP_SSE_AUTHORIZATION,
|
|
125
|
+
...(oidcEnabled ? { issuerOrigin: logSafeOrigin(env.OIDC_ISSUER ?? '') } : {}),
|
|
126
|
+
staticPrincipals: oidcEnabled ? 0 : countStaticPrincipals(env.MCP_STATIC_TOKENS),
|
|
127
|
+
},
|
|
128
|
+
tools: facts.tools,
|
|
129
|
+
budgets: {
|
|
130
|
+
count: inputs.budgets.count,
|
|
131
|
+
distinctServers: inputs.budgets.distinctServers,
|
|
132
|
+
schemes: [...inputs.budgets.schemes],
|
|
133
|
+
},
|
|
134
|
+
limits: {
|
|
135
|
+
opTimeoutMs: inputs.limits.opTimeoutMs,
|
|
136
|
+
maxSessions: inputs.limits.maxSessions,
|
|
137
|
+
idleTimeoutMs: inputs.limits.idleTimeoutMs,
|
|
138
|
+
...(http ? { keepAliveMs: http.listener.keepAliveMs } : {}),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -29,6 +29,7 @@ import { resolveStaticTokens, matchBearerToken, LEGACY_STATIC_PRINCIPAL } from '
|
|
|
29
29
|
import { runAclPreflight } from '../auth/budget-acl-dynamic.js';
|
|
30
30
|
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
|
|
31
31
|
import { resolveAllowedHosts } from '../lib/authPosture.js';
|
|
32
|
+
import { logSafeOrigin } from '../lib/budget-registry.js';
|
|
32
33
|
import * as https from 'node:https';
|
|
33
34
|
import { finished } from 'node:stream';
|
|
34
35
|
import * as fs from 'node:fs';
|
|
@@ -74,6 +75,8 @@ function capPrincipal(req) {
|
|
|
74
75
|
}
|
|
75
76
|
return req.staticPrincipal;
|
|
76
77
|
}
|
|
78
|
+
/** Idle keep-alive for the HTTP listener, in milliseconds. */
|
|
79
|
+
export const HTTP_KEEP_ALIVE_TIMEOUT_MS = 65000;
|
|
77
80
|
export async function startHttpServer(mcp, port, httpPath, capabilities, // was passed by index.ts
|
|
78
81
|
implementedTools, // was passed by index.ts
|
|
79
82
|
serverDescription, // was passed by index.ts
|
|
@@ -81,7 +84,7 @@ instructions, // was passed by index.ts (#560: built once there)
|
|
|
81
84
|
toolSchemas, // was passed by index.ts
|
|
82
85
|
version, // server version from package.json
|
|
83
86
|
bindHost = 'localhost', // #239: interface to bind; forwarded to listen()
|
|
84
|
-
advertisedUrl) {
|
|
87
|
+
advertisedUrl, onListening) {
|
|
85
88
|
// #572: resolve MCP_STATIC_TOKENS (token variables, budget names) before anything
|
|
86
89
|
// binds, so a bad entry refuses startup like an unreadable TLS key. The registry is
|
|
87
90
|
// read outside any request context, so it is the full, unfiltered list. config.ts
|
|
@@ -322,7 +325,7 @@ advertisedUrl) {
|
|
|
322
325
|
requiredScopes,
|
|
323
326
|
showErrorDetails: process.env.NODE_ENV !== 'production',
|
|
324
327
|
}), budgetAclMiddleware);
|
|
325
|
-
logger.info(`[OIDC] JWT authentication enabled. Issuer: ${config.OIDC_ISSUER}`);
|
|
328
|
+
logger.info(`[OIDC] JWT authentication enabled. Issuer: ${logSafeOrigin(config.OIDC_ISSUER ?? '')}`);
|
|
326
329
|
// #462 audit line: which header carries the JWT and where the keys come from.
|
|
327
330
|
logger.info('[OIDC] token source and JWKS mode', {
|
|
328
331
|
tokenSource: config.OIDC_TOKEN_SOURCE,
|
|
@@ -1122,7 +1125,14 @@ advertisedUrl) {
|
|
|
1122
1125
|
`MCP_HTTPS_CERT=${config.MCP_HTTPS_CERT}, MCP_HTTPS_KEY=${config.MCP_HTTPS_KEY}. Cause: ${cause}`);
|
|
1123
1126
|
}
|
|
1124
1127
|
}
|
|
1125
|
-
const listener = (tlsOptions ? https.createServer(tlsOptions, app) : app).listen(port, bindHost, () => {
|
|
1128
|
+
const listener = (tlsOptions ? https.createServer(tlsOptions, app) : app).listen(port, bindHost, (err) => {
|
|
1129
|
+
// #623: Express 5's app.listen also registers this callback as the server's 'error'
|
|
1130
|
+
// handler, so a taken port (EADDRINUSE) lands HERE with the error. Without this rethrow it
|
|
1131
|
+
// logged "listening", emitted sys_startup, and the process drained out with exit 0. The
|
|
1132
|
+
// rethrow reaches the uncaughtException handler (sys_crash, exit 1), the same path the
|
|
1133
|
+
// https.createServer branch takes, which has no error listener at all.
|
|
1134
|
+
if (err)
|
|
1135
|
+
throw err;
|
|
1126
1136
|
const advertised = advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`;
|
|
1127
1137
|
// #239: when bound to a specific (non all-interfaces) host the server is NOT on
|
|
1128
1138
|
// loopback, so point the health hint at the actual bind host instead of localhost.
|
|
@@ -1131,7 +1141,7 @@ advertisedUrl) {
|
|
|
1131
1141
|
// #625: one structured record through the module logger instead of three console.info lines.
|
|
1132
1142
|
httpLog.info(`MCP Streamable HTTP Server listening on ${port}; MCP endpoint: ${advertised} (health: ${health})`, { port, endpoint: advertised, health });
|
|
1133
1143
|
if (config.AUTH_PROVIDER === 'oidc') {
|
|
1134
|
-
logger.info(`🔒 OIDC authentication enabled (JWT Bearer token required; issuer: ${config.OIDC_ISSUER})`);
|
|
1144
|
+
logger.info(`🔒 OIDC authentication enabled (JWT Bearer token required; issuer: ${logSafeOrigin(config.OIDC_ISSUER ?? '')})`);
|
|
1135
1145
|
// #345: name the userNames the ACL can match, once, at boot. Deliberately
|
|
1136
1146
|
// fire-and-forget INSIDE the listen callback: the server is already
|
|
1137
1147
|
// accepting connections, so a slow or unreachable Actual delays nothing,
|
|
@@ -1150,11 +1160,22 @@ advertisedUrl) {
|
|
|
1150
1160
|
else {
|
|
1151
1161
|
logger.warn(`⚠️ HTTP authentication disabled (no MCP_SSE_AUTHORIZATION, MCP_STATIC_TOKENS or OIDC configured)`);
|
|
1152
1162
|
}
|
|
1163
|
+
// #623: keepAliveTimeout is assigned below, synchronously after listen() returns, so it is
|
|
1164
|
+
// already set when this callback fires. A throw from the record must never take down a
|
|
1165
|
+
// server that is already accepting connections.
|
|
1166
|
+
if (onListening) {
|
|
1167
|
+
try {
|
|
1168
|
+
onListening({ port, scheme, keepAliveMs: listener.keepAliveTimeout });
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
logger.warn('sys_startup record failed', { error: err instanceof Error ? err.name : 'unknown' });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1153
1174
|
});
|
|
1154
1175
|
// Configure keep-alive to maintain persistent connections
|
|
1155
|
-
|
|
1156
|
-
listener.
|
|
1157
|
-
|
|
1176
|
+
// #623: the value is reported in the sys_startup record (limits.keepAliveMs), not its own line.
|
|
1177
|
+
listener.keepAliveTimeout = HTTP_KEEP_ALIVE_TIMEOUT_MS; // slightly higher than typical client timeout
|
|
1178
|
+
listener.headersTimeout = HTTP_KEEP_ALIVE_TIMEOUT_MS + 1000; // must be higher than keepAliveTimeout
|
|
1158
1179
|
// Cleanup on server shutdown
|
|
1159
1180
|
const cleanup = async () => {
|
|
1160
1181
|
logger.info('[SERVER] Shutting down, cleaning up sessions...');
|
|
@@ -3,12 +3,7 @@ import { readFileSync } from 'fs';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import actualToolsManager from '../actualToolsManager.js';
|
|
6
|
-
import {
|
|
7
|
-
import { getMcpServerVersion } from '../lib/mcp-version.js';
|
|
8
|
-
/** Resolved once at module load, not per call: the walk does blocking file reads
|
|
9
|
-
* and this tool can be called in a loop. The SDK exports no root path, so the
|
|
10
|
-
* resolvable subpath is passed explicitly (#445). */
|
|
11
|
-
const MCP_SDK_RESOLVED = resolveInstalledVersion('@modelcontextprotocol/sdk', '@modelcontextprotocol/sdk/server/index.js');
|
|
6
|
+
import { collectVersionAndToolFacts } from '../lib/startupRecord.js';
|
|
12
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
8
|
const __dirname = dirname(__filename);
|
|
14
9
|
// Name, description and declared dependencies come from package.json. The version does
|
|
@@ -41,15 +36,22 @@ Use this to check server status, verify version compatibility, or debug issues.`
|
|
|
41
36
|
call: async (_args, _meta) => {
|
|
42
37
|
const uptime = process.uptime();
|
|
43
38
|
const uptimeFormatted = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${Math.floor(uptime % 60)}s`;
|
|
39
|
+
const policy = actualToolsManager.getToolPolicy();
|
|
40
|
+
// #623: the same resolved versions and counts the sys_startup record logs.
|
|
41
|
+
const facts = collectVersionAndToolFacts({
|
|
42
|
+
published: actualToolsManager.getPublishedToolNames().length,
|
|
43
|
+
registered: actualToolsManager.getToolNames().length,
|
|
44
|
+
policy,
|
|
45
|
+
});
|
|
44
46
|
return {
|
|
45
47
|
server: {
|
|
46
48
|
name: packageInfo.name,
|
|
47
|
-
version:
|
|
49
|
+
version: facts.versions.server,
|
|
48
50
|
description: packageInfo.description,
|
|
49
51
|
transport: process.env.MCP_STDIO_MODE === 'true' ? 'stdio' : 'http',
|
|
50
52
|
},
|
|
51
53
|
runtime: {
|
|
52
|
-
node:
|
|
54
|
+
node: facts.versions.node,
|
|
53
55
|
platform: process.platform,
|
|
54
56
|
arch: process.arch,
|
|
55
57
|
},
|
|
@@ -66,8 +68,8 @@ Use this to check server status, verify version compatibility, or debug issues.`
|
|
|
66
68
|
dependencies: {
|
|
67
69
|
mcpSdk: packageInfo.dependencies?.['@modelcontextprotocol/sdk'] ?? 'unknown',
|
|
68
70
|
actualApi: packageInfo.dependencies?.['@actual-app/api'] ?? 'unknown',
|
|
69
|
-
...(
|
|
70
|
-
...(
|
|
71
|
+
...(facts.versions.mcpSdk ? { mcpSdkResolved: facts.versions.mcpSdk } : {}),
|
|
72
|
+
...(facts.versions.actualApi ? { actualApiResolved: facts.versions.actualApi } : {}),
|
|
71
73
|
},
|
|
72
74
|
status: {
|
|
73
75
|
uptime: uptimeFormatted,
|
|
@@ -80,9 +82,9 @@ Use this to check server status, verify version compatibility, or debug issues.`
|
|
|
80
82
|
tools: {
|
|
81
83
|
// total is what this process PUBLISHES; registered is what exists. They differ
|
|
82
84
|
// when MCP_TOOLSETS, MCP_TOOLS or MCP_READ_ONLY narrows the surface (#483).
|
|
83
|
-
total:
|
|
84
|
-
registered:
|
|
85
|
-
policy
|
|
85
|
+
total: facts.tools.total,
|
|
86
|
+
registered: facts.tools.registered,
|
|
87
|
+
policy,
|
|
86
88
|
},
|
|
87
89
|
};
|
|
88
90
|
},
|
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.
|
|
4
|
+
"version": "0.27.0",
|
|
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/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",
|
|
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/startup_record.test.js && node tests/unit/startup_record_child.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",
|