actual-mcp-server 0.22.42 → 0.22.44

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
@@ -836,7 +836,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
836
836
 
837
837
  ---
838
838
 
839
- **Version:** 0.22.42 | **Tool Count:** 84 (verified LibreChat-compatible)
839
+ **Version:** 0.22.44 | **Tool Count:** 84 (verified LibreChat-compatible)
840
840
 
841
841
  ## Sponsor
842
842
 
package/dist/src/index.js CHANGED
@@ -9,6 +9,7 @@ import { getMcpServerVersion } from './lib/mcp-version.js';
9
9
  import path from 'node:path';
10
10
  import { z } from 'zod';
11
11
  import { isKnownBenignRejection } from './lib/rejection-allowlist.js';
12
+ import { buildServerInstructions } from './lib/serverInstructions.js';
12
13
  import { format } from 'node:util';
13
14
  /*
14
15
  * #277 / #541: this module's version comes from `getMcpServerVersion()` (src/lib/mcp-version.ts),
@@ -211,8 +212,6 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
211
212
  const useTestActualConnection = args.includes('--test-actual-connection');
212
213
  const useTestMcpClient = args.includes('--test-mcp-client');
213
214
  const SERVER_DESCRIPTION = 'Bridge MCP server exposing Actual finance API to LibreChat.';
214
- const SERVER_INSTRUCTIONS = 'Welcome to the Actual MCP server. The tools listed here are only the ones currently confirmed and tested, ' +
215
- 'but the server can proxy any API call supported by Actual. As we expand coverage, more tools will be officially exposed.';
216
215
  async function main() {
217
216
  // Mutual exclusion — stdio and http are incompatible transports
218
217
  if (useHttp && useStdio) {
@@ -234,6 +233,9 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
234
233
  // The PUBLISHED tools (#483). This is the one name list every tools/list path receives
235
234
  // (HTTP SDK handler, the no-session LobeChat path, the expired-session shim, stdio).
236
235
  const implementedTools = actualToolsManager.getPublishedToolNames();
236
+ // The initialize `instructions` (#560): built once from the published set and the same
237
+ // resolved policy, before any transport starts, so no transport can send a different text.
238
+ const serverInstructionsText = buildServerInstructions(implementedTools, actualToolsManager.getToolPolicy().readOnly);
237
239
  // Extract schemas map with tool name → JSON schema
238
240
  const toolSchemas = {};
239
241
  for (const toolName of implementedTools) {
@@ -314,7 +316,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
314
316
  logger.info('⚙️ --test-mcp-client specified, starting HTTP server and running client-side MCP tests...');
315
317
  // start server in http mode. Bind all interfaces by default (#239): advertisedUrl
316
318
  // uses the LAN IP (getLocalIp), so the self-test client must be able to reach it.
317
- await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version, bindHost, advertisedUrl);
319
+ await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, serverInstructionsText, toolSchemas, version, bindHost, advertisedUrl);
318
320
  // dynamic import to avoid circular at top
319
321
  const { testMcpClient } = await import('./tests/testMcpClient.js');
320
322
  try {
@@ -343,13 +345,13 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
343
345
  logger.info(`• OAuth Required: false`);
344
346
  logger.info(`• Capabilities: ${Object.keys(capabilities).join(', ')}`);
345
347
  logger.info(`• Tools: ${implementedTools.join(', ') || 'none'}`);
346
- logger.info(`• Server Instructions: ${SERVER_INSTRUCTIONS}`);
348
+ logger.info(`• Server Instructions: ${serverInstructionsText}`);
347
349
  logger.info(`• MCP endpoint (advertised): ${advertisedUrl}`);
348
350
  logger.info('---------');
349
351
  }
350
352
  if (useHttp) {
351
353
  logger.info('Mode: HTTP');
352
- await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version,
354
+ await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, serverInstructionsText, toolSchemas, version,
353
355
  // bind host (ensure server accepts connections on that interface)
354
356
  bindHost,
355
357
  // advertised URL shown to clients
@@ -357,7 +359,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
357
359
  }
358
360
  else if (useStdio) {
359
361
  logger.debug('Mode: stdio');
360
- await startStdioServer(mcp, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version);
362
+ await startStdioServer(mcp, capabilities, implementedTools, SERVER_DESCRIPTION, serverInstructionsText, toolSchemas, version);
361
363
  }
362
364
  else {
363
365
  logger.error('❌ Please specify a transport mode: --http or --stdio');
@@ -97,7 +97,6 @@ export class ActualMCPConnection extends EventEmitter {
97
97
  prompts: false,
98
98
  models: false,
99
99
  logging: false,
100
- serverInstructions: 'This server exposes Actual Finance tools via MCP. You must provide ACTUAL_SERVER_URL, ACTUAL_PASSWORD, and ACTUAL_BUDGET_SYNC_ID as environment variables.'
101
100
  };
102
101
  }
103
102
  /** Executes a tool requested by the client */
@@ -240,3 +240,63 @@ export function parseWhereClause(query, whereClause, tableName) {
240
240
  }
241
241
  return query;
242
242
  }
243
+ // #561: the joined name columns an empty-result hint can check, and the table that lists each.
244
+ const NAME_CONDITION_TABLES = {
245
+ 'category.name': { table: 'categories', noun: 'category' },
246
+ 'payee.name': { table: 'payees', noun: 'payee' },
247
+ 'account.name': { table: 'accounts', noun: 'account' },
248
+ };
249
+ /**
250
+ * #561: the `=`, `IN` and `LIKE` conditions on category.name, payee.name or account.name in a
251
+ * query built by parseWhereClause, read back from its serialized filterExpressions. `!=` and
252
+ * `NOT LIKE` are skipped: an unknown name there cannot make the result empty. Each condition is
253
+ * ONE lookup, an IN list included (the ticket allows one extra read per name condition), and the
254
+ * lookup reuses the main query's operator and value, so a `$oneof` element compiles exactly as in
255
+ * the main query (where upstream inlines the doubled-quote form #433 keeps). A numeric-looking
256
+ * literal arrives coerced to a number and is kept as is.
257
+ */
258
+ export function nameConditionLookups(filterExpressions) {
259
+ const out = [];
260
+ const isScalar = (v) => typeof v === 'string' || typeof v === 'number';
261
+ for (const expr of filterExpressions) {
262
+ if (expr === null || typeof expr !== 'object')
263
+ continue;
264
+ for (const [field, cond] of Object.entries(expr)) {
265
+ const target = NAME_CONDITION_TABLES[field];
266
+ if (!target)
267
+ continue;
268
+ if (isScalar(cond)) {
269
+ out.push({ ...target, nameFilter: cond, value: String(cond) });
270
+ }
271
+ else if (cond !== null && typeof cond === 'object') {
272
+ const c = cond;
273
+ if (typeof c.$like === 'string') {
274
+ out.push({ ...target, nameFilter: { $like: c.$like }, value: c.$like });
275
+ }
276
+ else if (Array.isArray(c.$oneof) && c.$oneof.length > 0 && c.$oneof.every(isScalar)) {
277
+ out.push({
278
+ ...target,
279
+ nameFilter: { $oneof: c.$oneof },
280
+ inValues: c.$oneof.map((v) => String(v).replace(/''/g, "'")),
281
+ });
282
+ }
283
+ }
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+ /**
289
+ * #561: the hint lines for one condition, given the names its lookup returned. Each value is
290
+ * echoed at most 100 characters. For an IN list the comparison is exact, because upstream compiles
291
+ * `$oneof` to a plain SQL IN, which is case-sensitive: 'groceries' does not match 'Groceries' in
292
+ * the main query, so it must not count as found here either (#561 review round 2).
293
+ */
294
+ export function nameConditionHintLines(lookup, foundNames) {
295
+ const line = (v) => `No ${lookup.noun} matches '${v.slice(0, 100)}'. ` +
296
+ `List the exact names with: SELECT id, name FROM ${lookup.table}`;
297
+ if (lookup.inValues) {
298
+ const found = new Set(foundNames);
299
+ return [...new Set(lookup.inValues)].filter((v) => !found.has(v)).map(line);
300
+ }
301
+ return foundNames.length === 0 ? [line(lookup.value ?? '')] : [];
302
+ }
@@ -3468,6 +3468,43 @@ export async function resetBudgetHold(month) {
3468
3468
  await withConcurrency(() => retry(() => rawResetBudgetHold(month), { retries: 2, backoffMs: 200 }));
3469
3469
  }, { preservesListings: PRESERVES_ALL_ENTITY_LISTINGS });
3470
3470
  }
3471
+ /**
3472
+ * #561: an empty result whose category.name, payee.name or account.name condition names no
3473
+ * existing entity gains a sibling `hint` naming the value and the query that lists the exact
3474
+ * names. Runs INSIDE runQuery's withActualApi session: raw reads only, never `adapter.*` (the api
3475
+ * lock is not reentrant). Each lookup is a `q()` filter object carrying the main query's own
3476
+ * operator and value, so the value never reaches a query string. A non-empty result, or one with
3477
+ * no name condition, is returned untouched with no lookup; a failed lookup drops the hint rather
3478
+ * than failing a query that succeeded.
3479
+ */
3480
+ async function withEmptyResultHint(result, query, q) {
3481
+ // Upstream answers `{ data }`; a bare array is tolerated the way getFinancialAnalysisSnapshot does.
3482
+ const rows = (r) => Array.isArray(r) ? r : Array.isArray(r?.data) ? r.data : undefined;
3483
+ const data = rows(result);
3484
+ if (!data || data.length > 0)
3485
+ return result;
3486
+ const lookups = nameConditionLookups(query.serialize?.().filterExpressions ?? []);
3487
+ if (lookups.length === 0)
3488
+ return result;
3489
+ try {
3490
+ const lines = [];
3491
+ for (const lookup of lookups) {
3492
+ const found = await withConcurrency(() => rawRunQuery(lookup.inValues
3493
+ ? q(lookup.table).filter({ name: lookup.nameFilter }).select(['name'])
3494
+ : q(lookup.table).filter({ name: lookup.nameFilter }).select(['name']).limit(1)));
3495
+ const names = (rows(found) ?? []).map((row) => String(row?.name ?? ''));
3496
+ lines.push(...nameConditionHintLines(lookup, names));
3497
+ }
3498
+ if (lines.length === 0)
3499
+ return result;
3500
+ const hint = lines.join('\n');
3501
+ return Array.isArray(result) ? { data: result, hint } : { ...result, hint };
3502
+ }
3503
+ catch (err) {
3504
+ logger.warn('[ADAPTER] Empty-result name lookup failed; returning the result without a hint', errorLogFields(err));
3505
+ return result;
3506
+ }
3507
+ }
3471
3508
  export async function runQuery(queryString) {
3472
3509
  try {
3473
3510
  return await withActualApi(async () => {
@@ -3510,6 +3547,7 @@ export async function runQuery(queryString) {
3510
3547
  }
3511
3548
  const trimmed = queryString.trim();
3512
3549
  let query;
3550
+ let hasWhereClause = false; // #561: only a parsed SQL WHERE can carry a name condition
3513
3551
  // Check for GraphQL-like query syntax: query Name { table(...) { fields } }
3514
3552
  const graphqlMatch = trimmed.match(/^query\s+\w+\s*\{\s*(\w+)\s*\(([^)]*)\)\s*\{([^}]+)\}\s*\}$/is);
3515
3553
  if (graphqlMatch) {
@@ -3577,6 +3615,7 @@ export async function runQuery(queryString) {
3577
3615
  // types and coerce boolean literals to real booleans (#420).
3578
3616
  if (whereClause) {
3579
3617
  query = parseWhereClause(query, whereClause, tableName);
3618
+ hasWhereClause = true;
3580
3619
  }
3581
3620
  // Apply ORDER BY
3582
3621
  if (orderField) {
@@ -3593,7 +3632,7 @@ export async function runQuery(queryString) {
3593
3632
  }
3594
3633
  }
3595
3634
  try {
3596
- return await withConcurrency(async () => {
3635
+ const result = await withConcurrency(async () => {
3597
3636
  try {
3598
3637
  return await rawRunQuery(query);
3599
3638
  }
@@ -3607,6 +3646,7 @@ export async function runQuery(queryString) {
3607
3646
  throw err;
3608
3647
  }
3609
3648
  });
3649
+ return hasWhereClause ? await withEmptyResultHint(result, query, q) : result;
3610
3650
  }
3611
3651
  catch (error) {
3612
3652
  // Enhance error messages with helpful context
@@ -3707,7 +3747,7 @@ export async function getFinancialAnalysisSnapshot(params) {
3707
3747
  }
3708
3748
  // WHERE-clause translation extracted to ./actual-adapter/query.ts (#166).
3709
3749
  // Imported for internal use by runQuery and re-exported (unit-tested directly).
3710
- import { parseWhereClause } from './actual-adapter/query.js';
3750
+ import { parseWhereClause, nameConditionLookups, nameConditionHintLines } from './actual-adapter/query.js';
3711
3751
  export { parseWhereClause };
3712
3752
  export async function runBankSync(accountId) {
3713
3753
  try {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The `instructions` string of the MCP `initialize` result (#560).
3
+ *
4
+ * ONE source, built from the tools this process actually publishes (#483), so the text can
5
+ * never point the model at a tool that MCP_TOOLSETS, MCP_TOOLS or MCP_READ_ONLY hid. Clients
6
+ * that read the field (LibreChat with `serverInstructions: true`, Claude Desktop, Claude Code,
7
+ * Gemini's MCP connector) inject it into the system prompt on every turn, so it holds
8
+ * cross-tool rules only: call order, the data conventions every tool shares, and how to
9
+ * recover. Facts that belong to one tool (join syntax, LIKE semantics, what an empty query
10
+ * result means) live in that tool's description and are deliberately not repeated here, where
11
+ * they would drift. The whole text stays at most 512 characters so it stands on its own in
12
+ * clients that truncate.
13
+ *
14
+ * Ids are promised for accounts and categories only: actual_get_context caps payees at
15
+ * payeeLimit (default 500) and its own description routes the rest elsewhere.
16
+ *
17
+ * Read-only comes from the resolved tool policy, never from tool annotations, which are hints
18
+ * that nothing in src/ may branch on.
19
+ */
20
+ export function buildServerInstructions(published, readOnly) {
21
+ const has = new Set(published);
22
+ const parts = [
23
+ 'This server exposes an Actual Budget through MCP tools.',
24
+ 'Amounts are integer cents (5000 is 50.00, negative is an outflow) and dates are YYYY-MM-DD strings.',
25
+ ];
26
+ if (has.has('actual_get_context')) {
27
+ parts.push('Call actual_get_context first and use the account and category ids it returns, never guessed names.');
28
+ if (has.has('actual_query_run')) {
29
+ parts.push('In actual_query_run, filter on the id columns (account, category, payee) rather than on names; an empty result can mean a name was wrong.');
30
+ }
31
+ }
32
+ if (readOnly) {
33
+ parts.push('This server is read-only, so do not offer to create, change or delete anything.');
34
+ }
35
+ return parts.join(' ');
36
+ }
@@ -54,7 +54,7 @@ function resolvePrincipal(req) {
54
54
  export async function startHttpServer(mcp, port, httpPath, capabilities, // was passed by index.ts
55
55
  implementedTools, // was passed by index.ts
56
56
  serverDescription, // was passed by index.ts
57
- serverInstructions, // was passed by index.ts
57
+ instructions, // was passed by index.ts (#560: built once there)
58
58
  toolSchemas, // was passed by index.ts
59
59
  version, // server version from package.json
60
60
  bindHost = 'localhost', // #239: interface to bind; forwarded to listen()
@@ -376,14 +376,11 @@ advertisedUrl) {
376
376
  const capabilitiesObj = capabilities && Object.keys(capabilities).length
377
377
  ? capabilities
378
378
  : { tools: toolsList.reduce((acc, n) => { acc[n] = {}; return acc; }, {}) };
379
+ // Only the two ServerOptions the MCP spec defines (#560). tools/list is served by the
380
+ // request handler, not by these options; the SDK ignored the extra keys this once passed.
379
381
  const serverOptions = {
380
- // Provide instructions and capabilities so the SDK initialize response is correct
381
- instructions: serverInstructions || "Welcome to the Actual MCP server.",
382
- serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
382
+ instructions,
383
383
  capabilities: capabilitiesObj,
384
- implementedTools: toolsList,
385
- // Include tools array explicitly so initialize result contains tools: string[]
386
- tools: toolsList,
387
384
  };
388
385
  const server = new Server({
389
386
  name: serverDescription || "actual-mcp-server",
@@ -915,8 +912,7 @@ advertisedUrl) {
915
912
  const serverIp = process.env.MCP_BRIDGE_PUBLIC_HOST || getLocalIp();
916
913
  const mcpInfo = () => ({
917
914
  description: serverDescription || "Actual MCP server",
918
- instructions: serverInstructions || "Welcome to the Actual MCP server.",
919
- serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
915
+ instructions,
920
916
  capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
921
917
  tools: toolsList,
922
918
  advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
@@ -85,7 +85,7 @@ export function createStdioCallToolHandler(mcp, stdioSessionId) {
85
85
  }
86
86
  };
87
87
  }
88
- export async function startStdioServer(mcp, capabilities, implementedTools, serverDescription, serverInstructions, toolSchemas, version) {
88
+ export async function startStdioServer(mcp, capabilities, implementedTools, serverDescription, instructions, toolSchemas, version) {
89
89
  const toolsList = Array.isArray(implementedTools) ? implementedTools : [];
90
90
  // #348: a synthetic session identity for this stdio process.
91
91
  //
@@ -108,7 +108,7 @@ export async function startStdioServer(mcp, capabilities, implementedTools, serv
108
108
  // restarts and can never collide with an HTTP session id.
109
109
  const stdioSessionId = `stdio-${randomUUID()}`;
110
110
  logger.debug(`[STDIO] session id ${stdioSessionId}`);
111
- const server = new Server({ name: serverDescription || 'actual-mcp-server', version: version || '0.1.0' }, { capabilities, instructions: serverInstructions });
111
+ const server = new Server({ name: serverDescription || 'actual-mcp-server', version: version || '0.1.0' }, { capabilities, instructions });
112
112
  attachTransportErrorLogger(server, 'stdio');
113
113
  // List tools handler — mirrors createServerInstance() in httpServer.ts
114
114
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -22,11 +22,9 @@ export async function testMcpClient(advertisedUrl, _port, httpPath) {
22
22
  const resultObj = resultRaw && typeof resultRaw === 'object' ? resultRaw : undefined;
23
23
  if (!resultObj)
24
24
  throw new Error('Probe result is not an object');
25
- const serverInstructions = resultObj['serverInstructions'];
26
- if (serverInstructions && typeof serverInstructions === 'object') {
27
- if (typeof serverInstructions['instructions'] !== 'string') {
28
- throw new Error('serverInstructions missing or wrong type');
29
- }
25
+ // #560: instructions is the spec's plain string, on /mcp-info as on initialize.
26
+ if (typeof resultObj['instructions'] !== 'string') {
27
+ throw new Error('Probe instructions missing or not a string');
30
28
  }
31
29
  const capabilitiesVal = resultObj['capabilities'];
32
30
  if (!capabilitiesVal || typeof capabilitiesVal !== 'object' || !('tools' in capabilitiesVal)) {
@@ -36,7 +34,7 @@ export async function testMcpClient(advertisedUrl, _port, httpPath) {
36
34
  if (!Array.isArray(toolsVal)) {
37
35
  throw new Error('tools missing or not array');
38
36
  }
39
- console.info('Probe OK: capabilities/tools/serverInstructions present');
37
+ console.info('Probe OK: capabilities/tools/instructions present');
40
38
  // Now do JSON-RPC initialize + tools/list using the HTTP endpoint
41
39
  const rpcUrl = `${base.origin}${httpPath}`;
42
40
  const initPayload = {
@@ -77,7 +75,7 @@ export async function testMcpClient(advertisedUrl, _port, httpPath) {
77
75
  }
78
76
  if (sessionId)
79
77
  console.info(`Received session id header: ${sessionId}`);
80
- // Validate initialize result fields (capabilities/tools/serverInstructions)
78
+ // Validate initialize result fields (capabilities/tools/instructions)
81
79
  const initResultRaw = initJson['result'];
82
80
  const initResult = initResultRaw && typeof initResultRaw === 'object' ? initResultRaw : undefined;
83
81
  if (!initResult || typeof initResult['capabilities'] !== 'object') {
@@ -90,21 +88,12 @@ export async function testMcpClient(advertisedUrl, _port, httpPath) {
90
88
  if (!hasTools) {
91
89
  throw new Error('initialize result.tools missing or not array and capabilities.tools not present');
92
90
  }
93
- // serverInstructions may be a string, an object { instructions }, or absent.
94
- const si = initResult['serverInstructions'];
95
- if (!si) {
96
- console.warn('Warning: serverInstructions missing from initialize result — continuing tests');
97
- }
98
- else if (typeof si === 'string') {
99
- // ok
100
- }
101
- else if (typeof si === 'object' && typeof si['instructions'] === 'string') {
102
- // ok
103
- }
104
- else {
105
- console.warn('Warning: serverInstructions present but in unexpected shape — continuing tests');
91
+ // #560: the spec's InitializeResult.instructions, a string. The harness never read the spec
92
+ // field before (it accepted a non-spec key in any shape), so this is now a hard check.
93
+ if (typeof initResult['instructions'] !== 'string') {
94
+ throw new Error('initialize result.instructions missing or not a string');
106
95
  }
107
- console.info('Initialize OK: capabilities/tools/serverInstructions present');
96
+ console.info('Initialize OK: capabilities/tools/instructions present');
108
97
  // call tools/list (JSON-RPC) to verify RPC path works
109
98
  const listPayload = { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} };
110
99
  const listHeaders = { ...commonPostHeaders };
@@ -15,6 +15,7 @@ Example: "SELECT id, date, amount, payee.name FROM transactions WHERE amount < 0
15
15
  WHERE supports: =, !=, >, >=, <, <=; IN (v1, v2); LIKE / NOT LIKE (case and accent-insensitive, % wildcard, e.g. imported_payee LIKE '%amazon%'); IS NULL / IS NOT NULL; boolean columns as true / false (cleared = false, category.hidden = true); conditions joined with AND. OR, REGEXP, NOT IN and parenthesised groups are not supported and return an error (a query is never silently run unfiltered). Transactions come back in split-INLINE mode: "is_parent = true" returns nothing, use "is_parent = false" to exclude split children.
16
16
 
17
17
  Joins use dot notation: payee.name, category.name, account.name (NOT payee_name). Amounts are in cents: $100.00 = 10000. Dates are quoted YYYY-MM-DD strings: date >= '2025-01-01'.
18
+ An empty result may carry a "hint" naming a category.name, payee.name or account.name value that matches no entity, with the query that lists the exact names.
18
19
 
19
20
  Tables:
20
21
  - transactions: id, date, amount, notes, cleared, account, payee, category
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.22.42",
4
+ "version": "0.22.44",
5
5
  "engines": {
6
6
  "node": ">=22.0.0",
7
7
  "npm": ">=10.0.0"
@@ -30,7 +30,7 @@
30
30
  "verify-tools": "npm run build && node scripts/verify-tools.js",
31
31
  "check:coverage": "node scripts/list-actual-api-methods.mjs",
32
32
  "direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
33
- "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js",
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",