actual-mcp-server 0.6.45 → 0.6.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -751,4 +751,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
751
751
 
752
752
  ---
753
753
 
754
- **Version:** 0.6.45 | **Tool Count:** 71 (verified LibreChat-compatible)
754
+ **Version:** 0.6.48 | **Tool Count:** 71 (verified LibreChat-compatible)
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.6.45",
4
+ "version": "0.6.48",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -30,7 +30,7 @@
30
30
  "verify-tools": "npm run build && node scripts/verify-tools.js",
31
31
  "check:coverage": "node scripts/list-actual-api-methods.mjs",
32
32
  "direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
33
- "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_insecure_upstream.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/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/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/payees_delete.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/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/entities_search.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/check_coverage.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_insecure_upstream.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/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/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/payees_delete.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/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/entities_search.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js",
34
34
  "test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
35
35
  "test:e2e": "npx playwright test",
36
36
  "test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
@@ -26,15 +26,19 @@ import logger from '../logger.js';
26
26
  */
27
27
  export function createModuleLogger(moduleName) {
28
28
  const prefix = `[${moduleName}]`;
29
+ // `module` is attached as a structured field (promoted to a top-level field by the
30
+ // logger format, #219) while the `[MODULE]` prefix is kept in the message for the
31
+ // pretty/dev output. `module` is spread LAST so it stays authoritative and a caller's
32
+ // metadata cannot accidentally overwrite it.
29
33
  return {
30
34
  info: (message, meta) => {
31
- logger.info(`${prefix} ${message}`, meta);
35
+ logger.info(`${prefix} ${message}`, { ...meta, module: moduleName });
32
36
  },
33
37
  debug: (message, meta) => {
34
- logger.debug(`${prefix} ${message}`, meta);
38
+ logger.debug(`${prefix} ${message}`, { ...meta, module: moduleName });
35
39
  },
36
40
  warn: (message, meta) => {
37
- logger.warn(`${prefix} ${message}`, meta);
41
+ logger.warn(`${prefix} ${message}`, { ...meta, module: moduleName });
38
42
  },
39
43
  error: (message, error, meta) => {
40
44
  if (error) {
@@ -42,10 +46,11 @@ export function createModuleLogger(moduleName) {
42
46
  error: error.message,
43
47
  stack: error.stack,
44
48
  ...meta,
49
+ module: moduleName,
45
50
  });
46
51
  }
47
52
  else {
48
- logger.error(`${prefix} ${message}`, meta);
53
+ logger.error(`${prefix} ${message}`, { ...meta, module: moduleName });
49
54
  }
50
55
  },
51
56
  };
@@ -2,6 +2,10 @@ import winston from 'winston';
2
2
  import DailyRotateFile from 'winston-daily-rotate-file';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import { randomUUID } from 'crypto';
6
+ // requestContext only imports async_hooks, so this is safe to import from the early-loaded
7
+ // logger module (no circular import).
8
+ import { requestContext } from './lib/requestContext.js';
5
9
  const __filename = fileURLToPath(import.meta.url);
6
10
  const __dirname = path.dirname(__filename);
7
11
  // env-configurable
@@ -14,7 +18,6 @@ const LOG_DIR = process.env.MCP_BRIDGE_LOG_DIR
14
18
  const DATE_PATTERN = process.env.MCP_BRIDGE_ROTATE_DATEPATTERN || 'YYYY-MM-DD';
15
19
  const MAX_SIZE = process.env.MCP_BRIDGE_MAX_LOG_SIZE || '20m';
16
20
  const MAX_FILES = process.env.MCP_BRIDGE_MAX_FILES || '14d';
17
- const DEFAULT_LEVEL = process.env.MCP_BRIDGE_LOG_LEVEL || 'debug';
18
21
  function safeStringify(obj, maxLen = 2000) {
19
22
  try {
20
23
  const seen = new WeakSet();
@@ -39,6 +42,228 @@ function safeStringify(obj, maxLen = 2000) {
39
42
  }
40
43
  }
41
44
  }
45
+ /**
46
+ * Resolve the output format, level, and service name from the environment.
47
+ *
48
+ * Format precedence is deterministic: an explicit `LOG_FORMAT` ("json" | "pretty")
49
+ * wins; otherwise `NODE_ENV==='production'` selects JSON; otherwise pretty. The
50
+ * non-production default is therefore the human-readable text format, so existing
51
+ * log-text-scraping tests keep passing and JSON is an explicit opt-in or prod-only.
52
+ */
53
+ export function resolveLogConfig(env = process.env) {
54
+ const fmt = String(env.LOG_FORMAT || '').toLowerCase();
55
+ const isProd = env.NODE_ENV === 'production';
56
+ const useJson = fmt === 'json' ? true : fmt === 'pretty' ? false : isProd;
57
+ const level = env.MCP_BRIDGE_LOG_LEVEL || (isProd ? 'info' : 'debug');
58
+ const service = env.MCP_SERVICE_NAME || 'actual-mcp-server';
59
+ return { useJson, level, service };
60
+ }
61
+ const { useJson: USE_JSON, level: DEFAULT_LEVEL, service: SERVICE } = resolveLogConfig(process.env);
62
+ // Fields that stay at the top level of a record; everything else is user metadata.
63
+ // `sessionId`/`requestId` are reserved correlation fields (#221), so they render top-level
64
+ // rather than nested under `context`.
65
+ const STANDARD_KEYS = new Set([
66
+ 'level', 'message', 'timestamp', 'service', 'module', 'stack', 'ms', 'sessionId', 'requestId',
67
+ ]);
68
+ // Promote a `service` field onto every record.
69
+ const addService = winston.format((info) => {
70
+ info.service = SERVICE;
71
+ return info;
72
+ });
73
+ /**
74
+ * Resolve a request correlation id: reuse a non-empty inbound `X-Correlation-ID`, else
75
+ * generate one. Exported so the HTTP layer and tests share the exact decision (#221).
76
+ *
77
+ * The inbound value is CLIENT-CONTROLLED and is stamped on every log line for the request,
78
+ * so it is sanitised first: control characters are stripped (prevents log-line forging in
79
+ * non-JSON consumers) and the length is capped (prevents per-line bloat from a hostile
80
+ * header). If nothing usable remains, a UUID is generated.
81
+ */
82
+ const MAX_REQUEST_ID_LEN = 128;
83
+ export function resolveRequestId(inbound) {
84
+ let id = typeof inbound === 'string' ? inbound.trim() : '';
85
+ if (id) {
86
+ // eslint-disable-next-line no-control-regex
87
+ id = id.replace(/[\u0000-\u001f\u007f]/g, '').slice(0, MAX_REQUEST_ID_LEN);
88
+ }
89
+ return id.length > 0 ? id : randomUUID();
90
+ }
91
+ // Stamp the active request's correlation ids onto every record (#221). Reads the
92
+ // AsyncLocalStorage store; the context-derived id is authoritative, so it OVERWRITES any
93
+ // caller-supplied `sessionId` metadata. When there is no active store (startup, CLI, stdio
94
+ // without a request wrapper) it is a no-op and never throws. Must run after splat() (which
95
+ // reverts top-level field additions made before it), so it is placed alongside addService.
96
+ const addCorrelation = winston.format((info) => {
97
+ const store = requestContext.getStore();
98
+ if (store?.sessionId)
99
+ info.sessionId = store.sessionId;
100
+ if (store?.requestId)
101
+ info.requestId = store.requestId;
102
+ return info;
103
+ });
104
+ // Collect non-standard fields into a single `context` object so JSON records have a
105
+ // stable shape ({ timestamp, level, service, module, message, stack?, context? }).
106
+ // winston hands the SAME info object to every transport's format, so this must be
107
+ // idempotent: a record passing through more than one transport (e.g. STORE_LOGS=true
108
+ // gives two file transports plus the console) must not re-nest `context` into
109
+ // `context.context`. Excluding the `context` key from the move makes a second pass a
110
+ // no-op, and any already-present context is merged rather than wrapped.
111
+ const nestContext = winston.format((info) => {
112
+ const ctx = {};
113
+ for (const key of Object.keys(info)) {
114
+ if (!STANDARD_KEYS.has(key) && key !== 'context') {
115
+ ctx[key] = info[key];
116
+ delete info[key];
117
+ }
118
+ }
119
+ if (Object.keys(ctx).length > 0) {
120
+ const existing = info.context;
121
+ info.context =
122
+ existing && typeof existing === 'object' ? { ...existing, ...ctx } : ctx;
123
+ }
124
+ return info;
125
+ });
126
+ // ---------------------------------------------------------------------------
127
+ // Secret / PII redaction (#220)
128
+ //
129
+ // Runs inside buildLogFormat (per-transport), specifically AFTER winston.format.splat():
130
+ // splat() rebuilds the record from the original args and reverts top-level field mutations
131
+ // made before it (nested objects survive only because they are shared refs), so redaction
132
+ // placed before splat would silently fail to mask a top-level secret like `note`. Running
133
+ // it per-transport in the SAME pipeline that serializes (before nestContext, which moves
134
+ // fields under `context`) is the placement that actually reaches the output. It is
135
+ // idempotent (masking and value-scrub both go to a constant), so the STORE_LOGS
136
+ // multi-transport fan-out is safe. The live `note`/header tests pin this ordering: moving
137
+ // redactSecrets before splat makes them fail.
138
+ //
139
+ // Two mechanisms: a key-name denylist for structured fields at any depth, and a finite
140
+ // scrub of the actual configured secret VALUES (which catches a secret carried as a value
141
+ // under a benign key, or inside a stringified message).
142
+ // ---------------------------------------------------------------------------
143
+ const REDACTED = '[REDACTED]';
144
+ const SENSITIVE_KEYS = new Set([
145
+ 'authorization', 'proxy-authorization', 'token', 'password', 'encryptionpassword',
146
+ 'cookie', 'set-cookie', 'secret', 'apikey', 'api_key', 'x-api-key',
147
+ 'access_token', 'refresh_token', 'client_secret',
148
+ ]);
149
+ /** A metadata key is sensitive if its lowercased name is denylisted or ends in a secret suffix. */
150
+ export function isSensitiveKey(key) {
151
+ const low = key.toLowerCase();
152
+ if (SENSITIVE_KEYS.has(low))
153
+ return true;
154
+ return (low.endsWith('password') ||
155
+ low.endsWith('secret') ||
156
+ low.endsWith('token') ||
157
+ low.includes('authorization') ||
158
+ low.includes('cookie'));
159
+ }
160
+ /** The actual configured secret values, so a secret echoed as a VALUE is also masked. */
161
+ export function collectSecretValues(env = process.env) {
162
+ const out = [];
163
+ for (const [k, v] of Object.entries(env)) {
164
+ // length guard avoids masking short/common values that could collide with benign log
165
+ // text (a short secret is still masked by KEY name when under a sensitive field; only
166
+ // the free-text VALUE scrub is skipped for it).
167
+ if (typeof v === 'string' && v.length >= 8 && /(PASSWORD|SECRET|AUTHORIZATION|_TOKEN)$/.test(k)) {
168
+ out.push(v);
169
+ }
170
+ }
171
+ return out;
172
+ }
173
+ function scrubSecretValues(s, secrets) {
174
+ let out = s;
175
+ for (const sec of secrets) {
176
+ if (sec && out.includes(sec))
177
+ out = out.split(sec).join(REDACTED);
178
+ }
179
+ return out;
180
+ }
181
+ const MAX_REDACT_DEPTH = 8;
182
+ const MAX_REDACT_NODES = 2000;
183
+ /**
184
+ * Return a redacted version of a value. NESTED objects/arrays are COPIED (copy-on-write),
185
+ * never mutated, so redacting a log record cannot corrupt a live caller object (e.g.
186
+ * `req.headers` passed by reference). Depth/node caps and a cycle guard bound pathological
187
+ * or cyclic structures.
188
+ */
189
+ function redactValue(val, secrets, depth, seen, counter) {
190
+ if (val && typeof val === 'object') {
191
+ if (depth > MAX_REDACT_DEPTH || counter.n > MAX_REDACT_NODES)
192
+ return '[Truncated]';
193
+ if (seen.has(val))
194
+ return '[Circular]';
195
+ seen.add(val);
196
+ if (Array.isArray(val))
197
+ return val.map((v) => redactValue(v, secrets, depth + 1, seen, counter));
198
+ const copy = {};
199
+ for (const key of Object.keys(val)) {
200
+ counter.n++;
201
+ copy[key] = isSensitiveKey(key)
202
+ ? REDACTED
203
+ : redactValue(val[key], secrets, depth + 1, seen, counter);
204
+ }
205
+ return copy;
206
+ }
207
+ if (typeof val === 'string' && secrets.length > 0)
208
+ return scrubSecretValues(val, secrets);
209
+ return val;
210
+ }
211
+ /**
212
+ * Mask sensitive keys and scrub known-secret values. The TOP-LEVEL record is mutated in
213
+ * place (winston hands the transport its own copy, and the winston Symbol keys must be
214
+ * preserved), but every NESTED object is replaced with a redacted copy so a shared live
215
+ * object (a request's headers) is never altered. Masking to a constant is idempotent.
216
+ */
217
+ export function redactRecord(record, secrets = []) {
218
+ if (!record || typeof record !== 'object')
219
+ return record;
220
+ const seen = new WeakSet();
221
+ const counter = { n: 0 };
222
+ const rec = record;
223
+ for (const key of Object.keys(rec)) {
224
+ rec[key] = isSensitiveKey(key) ? REDACTED : redactValue(rec[key], secrets, 1, seen, counter);
225
+ }
226
+ return record;
227
+ }
228
+ const SECRET_VALUES = collectSecretValues(process.env);
229
+ const redactSecrets = winston.format((info) => redactRecord(info, SECRET_VALUES));
230
+ /**
231
+ * Build the winston format. Reuses winston's own building blocks
232
+ * (`format.errors` + `format.splat` + `format.json`) rather than a hand-rolled
233
+ * serializer, so the metadata that loggerFactory already attaches flows through.
234
+ * The previous printf referenced only `{ timestamp, level, message }`, which is
235
+ * why metadata (including error stacks) was silently dropped.
236
+ */
237
+ export function buildLogFormat(useJson, colorize = !!process.stdout.isTTY) {
238
+ if (useJson) {
239
+ return winston.format.combine(winston.format.timestamp(), // ISO 8601 UTC
240
+ winston.format.errors({ stack: true }), winston.format.splat(),
241
+ // #220: redact AFTER splat. splat() rebuilds the record from the original args and
242
+ // reverts top-level field mutations made before it (nested objects survive as shared
243
+ // refs), so redaction must run after splat to actually mask top-level secrets.
244
+ redactSecrets(), addCorrelation(), // #221: stamp sessionId/requestId (after splat, like addService)
245
+ addService(), nestContext(), winston.format.json());
246
+ }
247
+ // Colorize only for an interactive terminal, so piped output and log files do not get
248
+ // ANSI escape codes (also keeps the format deterministic and unit-testable).
249
+ return winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.errors({ stack: true }), winston.format.splat(), redactSecrets(), // #220: after splat (see note in the json branch)
250
+ addCorrelation(), // #221: stamp sessionId/requestId
251
+ ...(colorize ? [winston.format.colorize()] : []), winston.format.printf((info) => {
252
+ const rec = info;
253
+ let line = `${rec.timestamp} ${rec.level}: ${rec.message}`;
254
+ if (rec.stack)
255
+ line += `\n${rec.stack}`;
256
+ // Append any remaining metadata for dev readability (it is no longer dropped).
257
+ const extra = {};
258
+ for (const key of Object.keys(rec)) {
259
+ if (!STANDARD_KEYS.has(key))
260
+ extra[key] = rec[key];
261
+ }
262
+ if (Object.keys(extra).length > 0)
263
+ line += ` ${safeStringify(extra, 1000)}`;
264
+ return line;
265
+ }));
266
+ }
42
267
  const transports = [];
43
268
  // file transports when enabled (capture debug+)
44
269
  if (STORE_LOGS) {
@@ -50,22 +275,23 @@ if (STORE_LOGS) {
50
275
  zippedArchive: true,
51
276
  maxSize: MAX_SIZE,
52
277
  maxFiles: MAX_FILES,
53
- format: winston.format.combine(winston.format.timestamp(), winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`)),
278
+ // Files must never get ANSI colour, regardless of whether stdout is a TTY.
279
+ format: buildLogFormat(USE_JSON, false),
54
280
  });
55
281
  // debug transport collects debug+ messages into debug-%DATE%.log
56
282
  transports.push(createDailyRotateTransport('debug'));
57
283
  // errors into error-%DATE%.log
58
284
  transports.push(createDailyRotateTransport('error'));
59
285
  }
60
- // single console transport for terminal output (use same level)
61
- // In stdio mode all output must go to stderr — writing to stdout corrupts JSON-RPC framing.
286
+ // single console transport for terminal output (use same level).
287
+ // In stdio mode all output must go to stderr. Writing to stdout corrupts JSON-RPC framing.
62
288
  // MCP_STDIO_MODE is set by src/index.ts before this module is first imported.
63
289
  transports.push(new winston.transports.Console({
64
290
  level: DEFAULT_LEVEL,
65
291
  ...(process.env.MCP_STDIO_MODE === 'true'
66
292
  ? { stderrLevels: ['error', 'warn', 'info', 'verbose', 'debug', 'silly', 'http'] }
67
293
  : {}),
68
- format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.colorize(), winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`)),
294
+ format: buildLogFormat(USE_JSON),
69
295
  }));
70
296
  const logger = winston.createLogger({
71
297
  level: DEFAULT_LEVEL,
@@ -73,15 +299,17 @@ const logger = winston.createLogger({
73
299
  exitOnError: false,
74
300
  });
75
301
  export function logTransportWithDirection(direction, clientIp, req, data) {
76
- const meta = {
302
+ // Log STRUCTURED meta (not a pre-stringified blob) so the redaction format (#220) can
303
+ // mask sensitive header keys (Authorization, Cookie) and known-secret values in the
304
+ // payload before anything is written.
305
+ logger.debug('transport message', {
77
306
  direction,
78
307
  clientIp,
79
308
  method: req.method,
80
309
  url: req.originalUrl,
81
310
  headers: req.headers,
82
311
  payload: safeStringify(data, 2000),
83
- };
84
- logger.debug(safeStringify(meta, 4000));
312
+ });
85
313
  }
86
314
  // --- Wire debug module into winston and route console.* to winston only ---
87
315
  // NOTE: avoid writing to both original console and winston to prevent duplicates.
@@ -121,7 +349,7 @@ export function logTransportWithDirection(direction, clientIp, req, data) {
121
349
  }
122
350
  }
123
351
  catch {
124
- // debug module not available or failed to patch — ignore
352
+ // debug module not available or failed to patch, ignore
125
353
  }
126
354
  })();
127
355
  export default logger;
@@ -3,7 +3,7 @@ import { randomUUID, timingSafeEqual } from 'node:crypto';
3
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
5
  import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
- import logger from '../logger.js';
6
+ import logger, { resolveRequestId } from '../logger.js';
7
7
  import { getLocalIp } from '../utils.js';
8
8
  import actualToolsManager from '../actualToolsManager.js';
9
9
  import { getConnectionState, connectToActualForSession, shutdownActualForSession, shutdownActual, canAcceptNewSession } from '../actualConnection.js';
@@ -389,7 +389,8 @@ bindHost = 'localhost', advertisedUrl) {
389
389
  // Run in AsyncLocalStorage context so tools can access sessionId
390
390
  // and the adapter can enforce per-request budget ACL (#156).
391
391
  const allowedBudgetsInit = req.allowedBudgets;
392
- await requestContext.run({ sessionId: undefined, allowedBudgets: allowedBudgetsInit, principal: resolvePrincipal(req) }, async () => {
392
+ const requestId = resolveRequestId(req.get('x-correlation-id'));
393
+ await requestContext.run({ sessionId: undefined, requestId, allowedBudgets: allowedBudgetsInit, principal: resolvePrincipal(req) }, async () => {
393
394
  await transport.handleRequest(req, res, req.body);
394
395
  });
395
396
  }
@@ -483,7 +484,8 @@ bindHost = 'localhost', advertisedUrl) {
483
484
  // Run in AsyncLocalStorage context so tools and the adapter can access
484
485
  // sessionId (pool branch, #134) and allowedBudgets (ACL enforcement, #156).
485
486
  const allowedBudgets = req.allowedBudgets;
486
- await requestContext.run({ sessionId, allowedBudgets, principal: resolvePrincipal(req) }, async () => {
487
+ const requestId = resolveRequestId(req.get('x-correlation-id'));
488
+ await requestContext.run({ sessionId, requestId, allowedBudgets, principal: resolvePrincipal(req) }, async () => {
487
489
  await transport.handleRequest(req, res, req.body);
488
490
  });
489
491
  }
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.6.45",
4
+ "version": "0.6.48",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -30,7 +30,7 @@
30
30
  "verify-tools": "npm run build && node scripts/verify-tools.js",
31
31
  "check:coverage": "node scripts/list-actual-api-methods.mjs",
32
32
  "direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
33
- "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_insecure_upstream.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/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/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/payees_delete.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/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/entities_search.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/check_coverage.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_insecure_upstream.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/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/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/payees_delete.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/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.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/entities_search.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js",
34
34
  "test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
35
35
  "test:e2e": "npx playwright test",
36
36
  "test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",