actual-mcp-server 0.22.17 → 0.22.18

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
@@ -835,7 +835,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
835
835
 
836
836
  ---
837
837
 
838
- **Version:** 0.22.17 | **Tool Count:** 83 (verified LibreChat-compatible)
838
+ **Version:** 0.22.18 | **Tool Count:** 83 (verified LibreChat-compatible)
839
839
 
840
840
  ## Sponsor
841
841
 
@@ -165,6 +165,10 @@ export async function shutdownActualForSession(sessionId) {
165
165
  logger.error(`Error shutting down Actual for session ${sessionId}:`, err);
166
166
  }
167
167
  }
168
+ function poolSummary() {
169
+ const { sessions: _sessions, ...summary } = connectionPool.getStats();
170
+ return summary;
171
+ }
168
172
  export function getConnectionState() {
169
173
  // When using connection pooling, consider server initialized if pool is ready
170
174
  // (even with 0 active connections, pool being initialized means server is ready to accept sessions)
@@ -173,7 +177,9 @@ export function getConnectionState() {
173
177
  return {
174
178
  initialized: effectivelyInitialized,
175
179
  initializationError,
176
- connectionPool: useConnectionPool ? connectionPool.getStats() : null,
180
+ // Aggregate figures only. This feeds the unauthenticated /health route, so the
181
+ // per-session list (full session ids) is left out on purpose.
182
+ connectionPool: useConnectionPool ? poolSummary() : null,
177
183
  idleTimeoutMinutes: useConnectionPool ? connectionPool.getIdleTimeoutMinutes() : null,
178
184
  };
179
185
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Which principal created each MCP session.
3
+ *
4
+ * An MCP session id is bearer material for the Streamable HTTP transport, so a
5
+ * session must only be usable, listable and closable by the principal that
6
+ * created it. The principal is the one `resolvePrincipal` in httpServer.ts
7
+ * derives from the verified request (the OIDC subject, `static-bearer`, or
8
+ * undefined when auth is disabled), never a client-supplied value.
9
+ *
10
+ * This lives beside the transport table rather than on the connection-pool
11
+ * entry on purpose: a pool entry is dropped and recreated during the life of
12
+ * one MCP session (a budget switch onto another server, a transient upstream
13
+ * error), and an owner stored there would vanish with it. The transport's
14
+ * lifetime IS the session's lifetime, so httpServer records the owner when it
15
+ * registers the transport and forgets it when the pool evicts the session.
16
+ *
17
+ * Lives in src/lib/ so the session tools can import it without importing the
18
+ * HTTP server.
19
+ */
20
+ const owners = new Map();
21
+ export function recordSessionOwner(sessionId, principal) {
22
+ owners.set(sessionId, principal);
23
+ }
24
+ export function forgetSessionOwner(sessionId) {
25
+ owners.delete(sessionId);
26
+ }
27
+ /**
28
+ * True when `principal` may use `sessionId`.
29
+ *
30
+ * A session with no recorded owner is treated as owned by "no principal"
31
+ * (undefined). In auth-disabled and stdio modes every caller is that same
32
+ * undefined principal, so nothing changes there; under OIDC or a static bearer
33
+ * every caller has a principal, so an unrecorded session matches nobody.
34
+ */
35
+ export function sessionOwnerMatches(sessionId, principal) {
36
+ return owners.get(sessionId) === principal;
37
+ }
38
+ /** Keep only the sessions `principal` owns. */
39
+ export function filterOwnedSessions(sessions, principal) {
40
+ return sessions.filter((s) => sessionOwnerMatches(s.sessionId, principal));
41
+ }
@@ -28,6 +28,7 @@ import * as fs from 'node:fs';
28
28
  // Re-exported here for backward compatibility with any callers that imported
29
29
  // `requestContext` from this module.
30
30
  import { requestContext } from '../lib/requestContext.js';
31
+ import { recordSessionOwner, forgetSessionOwner, sessionOwnerMatches } from '../lib/session-owners.js';
31
32
  import { buildToolListEntries } from '../lib/tool-list-entry.js';
32
33
  // #438 / #452: the init-failure classifier now lives in src/lib/init-failure.ts so BOTH
33
34
  // transports can reach it (stdio could not, which was the whole of #452). Re-exported here
@@ -288,6 +289,7 @@ advertisedUrl) {
288
289
  }
289
290
  transports.delete(sessionId);
290
291
  sessionInitPromises.delete(sessionId);
292
+ forgetSessionOwner(sessionId);
291
293
  logger.info(`[SESSION] Transport torn down for evicted session: ${sessionId}`);
292
294
  });
293
295
  // Authentication middleware
@@ -488,6 +490,9 @@ advertisedUrl) {
488
490
  // onsessioninitialized catch block below: we deliberately do NOT re-log here
489
491
  // to avoid duplicate noise and any risk of leaking credentials.
490
492
  initPromise.catch(() => { });
493
+ // The session belongs to the principal that initialized it. Resolved here,
494
+ // from this request, so the owner can never come from a later request.
495
+ const sessionOwner = resolvePrincipal(req);
491
496
  const transport = new StreamableHTTPServerTransport({
492
497
  sessionIdGenerator: () => randomUUID(),
493
498
  enableJsonResponse: true,
@@ -501,6 +506,7 @@ advertisedUrl) {
501
506
  // Only register the transport if the pool connection succeeded.
502
507
  // The pool stamped lastActivity when it created the entry, so it
503
508
  // is already the source of truth for this session's idle clock.
509
+ recordSessionOwner(sid, sessionOwner);
504
510
  transports.set(sid, transport);
505
511
  logger.info(`[SESSION] Actual connection initialized for session: ${sid}`);
506
512
  resolveInit?.();
@@ -642,6 +648,31 @@ advertisedUrl) {
642
648
  return;
643
649
  }
644
650
  }
651
+ // A session is usable only by the principal that created it. Another
652
+ // principal gets exactly the "Session not found" answer an unknown id gets,
653
+ // so the response does not confirm that the id exists. That includes the
654
+ // tools/list discovery fallback above: answering it with a 404 here while
655
+ // an unknown id gets the tool list would itself confirm the id is live.
656
+ if (!sessionOwnerMatches(sessionId, resolvePrincipal(req))) {
657
+ logger.warn(`[SESSION] Refused a request for session ${sessionId} from a principal that does not own it (method: ${method})`);
658
+ if (method === 'tools/list') {
659
+ res.json({
660
+ jsonrpc: '2.0',
661
+ id: payload?.id ?? null,
662
+ result: { tools: buildToolListEntries(toolsList, resolveToolMeta) },
663
+ });
664
+ return;
665
+ }
666
+ res.status(404).json({
667
+ jsonrpc: '2.0',
668
+ id: payload?.id ?? null,
669
+ error: {
670
+ code: -32001,
671
+ message: 'Session not found. Please re-initialize by calling initialize without mcp-session-id header.'
672
+ },
673
+ });
674
+ return;
675
+ }
645
676
  // Refresh the pool's idle clock for this session (single source of truth, #167).
646
677
  connectionPool.touch(sessionId);
647
678
  // Run in AsyncLocalStorage context so tools and the adapter can access
@@ -700,6 +731,14 @@ advertisedUrl) {
700
731
  res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'No session id' }, id: null });
701
732
  return;
702
733
  }
734
+ // Same ownership rule as the POST route, checked before the idle clock is
735
+ // touched so another principal cannot keep a session alive either. A refusal
736
+ // reads like an unknown session.
737
+ if (transports.has(sessionId) && !sessionOwnerMatches(sessionId, resolvePrincipal(req))) {
738
+ logger.warn(`[SESSION] Refused a GET for session ${sessionId} from a principal that does not own it`);
739
+ res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Transport not ready' }, id: null });
740
+ return;
741
+ }
703
742
  connectionPool.touch(sessionId); // Refresh the pool's idle clock (#167)
704
743
  const transport = transports.get(sessionId);
705
744
  if (!transport) {
@@ -760,6 +799,9 @@ advertisedUrl) {
760
799
  });
761
800
  app.get('/mcp-info', (_req, res) => { res.json(mcpInfo()); });
762
801
  app.get('/mcp-info/http', (_req, res) => { res.json(mcpInfo()); });
802
+ // /health is unauthenticated, so it reports counts only. Session ids are bearer
803
+ // material for the transport and must never appear here; getConnectionState
804
+ // returns the pool's aggregate figures without the per-session list.
763
805
  app.get('/health', (_req, res) => {
764
806
  const state = getConnectionState();
765
807
  const poolStats = state.connectionPool || null;
@@ -2,16 +2,25 @@ import { z } from 'zod';
2
2
  import { connectionPool } from '../lib/ActualConnectionPool.js';
3
3
  import { shutdownActualForSession } from '../actualConnection.js';
4
4
  import { clearSessionBudgetState } from '../lib/actual-adapter.js';
5
+ import { requestContext } from '../lib/requestContext.js';
6
+ import { filterOwnedSessions } from '../lib/session-owners.js';
5
7
  const InputSchema = z.object({
6
- sessionId: z.string().optional().describe('Session ID to close (partial match). If not provided, closes the oldest idle session.'),
8
+ sessionId: z.string().optional().describe('Session ID to close (partial match among your own sessions). If not provided, closes your oldest idle session.'),
7
9
  });
8
10
  const tool = {
9
11
  name: 'actual_session_close',
10
- description: 'Close an idle MCP session to free up connection slots. Useful when you get "Max concurrent sessions reached" errors. Only closes sessions other than the current one.',
12
+ description: 'Close one of your idle MCP sessions to free up connection slots. Useful when you get "Max concurrent sessions reached" errors. Only closes sessions you opened, other than the current one.',
11
13
  inputSchema: InputSchema,
12
14
  call: async (args, _meta) => {
13
15
  const input = InputSchema.parse(args || {});
14
16
  const stats = connectionPool.getStats();
17
+ // Get the current session and principal from the request context: the
18
+ // session to protect from closing itself, and the owner to scope by.
19
+ const context = requestContext.getStore();
20
+ const currentSessionId = context?.sessionId;
21
+ // Only the caller's own sessions are candidates, and only their ids appear in
22
+ // any reply. Partial matching stays, because it runs over this list alone.
23
+ const ownSessions = filterOwnedSessions(stats.sessions, context?.principal);
15
24
  if (stats.totalSessions === 0) {
16
25
  return {
17
26
  success: false,
@@ -20,20 +29,16 @@ const tool = {
20
29
  maxConcurrent: stats.maxConcurrent,
21
30
  };
22
31
  }
23
- // Get current session ID from request context to prevent closing own session
24
- const { requestContext } = await import('../server/httpServer.js');
25
- const context = requestContext.getStore();
26
- const currentSessionId = context?.sessionId;
27
32
  // Find session to close
28
33
  let targetSessionId = null;
29
34
  if (input.sessionId) {
30
35
  // Find session by partial match
31
- const matchingSessions = stats.sessions.filter(s => s.sessionId.toLowerCase().includes(input.sessionId.toLowerCase()));
36
+ const matchingSessions = ownSessions.filter(s => s.sessionId.toLowerCase().includes(input.sessionId.toLowerCase()));
32
37
  if (matchingSessions.length === 0) {
33
38
  return {
34
39
  success: false,
35
40
  message: `No session found matching "${input.sessionId}"`,
36
- availableSessions: stats.sessions.map(s => s.sessionId),
41
+ availableSessions: ownSessions.map(s => s.sessionId),
37
42
  };
38
43
  }
39
44
  if (matchingSessions.length > 1) {
@@ -47,7 +52,7 @@ const tool = {
47
52
  }
48
53
  else {
49
54
  // Close the oldest idle session (not current session)
50
- const sortedSessions = [...stats.sessions]
55
+ const sortedSessions = [...ownSessions]
51
56
  .filter(s => !currentSessionId || !s.sessionId.includes(currentSessionId))
52
57
  .sort((a, b) => b.idleMinutes - a.idleMinutes);
53
58
  if (sortedSessions.length === 0) {
@@ -76,7 +81,7 @@ const tool = {
76
81
  return {
77
82
  success: false,
78
83
  message: `Session ${targetSessionId} not found in connection pool`,
79
- availableSessions: stats.sessions.map(s => s.sessionId),
84
+ availableSessions: ownSessions.map(s => s.sessionId),
80
85
  };
81
86
  }
82
87
  await shutdownActualForSession(targetSessionId);
@@ -1,18 +1,23 @@
1
1
  import { z } from 'zod';
2
2
  import { connectionPool } from '../lib/ActualConnectionPool.js';
3
+ import { requestContext } from '../lib/requestContext.js';
4
+ import { filterOwnedSessions } from '../lib/session-owners.js';
3
5
  const InputSchema = z.object({});
4
6
  const tool = {
5
7
  name: 'actual_session_list',
6
- description: 'List all active MCP sessions with their activity status. Useful for diagnosing connection issues or seeing which sessions can be closed.',
8
+ description: 'List your active MCP sessions with their activity status. Useful for diagnosing connection issues or seeing which sessions can be closed. The pool totals cover every session; the list shows only sessions you opened.',
7
9
  inputSchema: InputSchema,
8
10
  call: async (_args) => {
9
11
  const stats = connectionPool.getStats();
12
+ // Session ids are bearer material: list only the caller's own sessions. The
13
+ // totals stay pool-wide because they explain a "max concurrent sessions" refusal.
14
+ const own = filterOwnedSessions(stats.sessions, requestContext.getStore()?.principal);
10
15
  return {
11
16
  totalSessions: stats.totalSessions,
12
17
  activeSessions: stats.activeSessions,
13
18
  maxConcurrent: stats.maxConcurrent,
14
19
  availableSlots: stats.maxConcurrent - stats.activeSessions,
15
- sessions: stats.sessions.map(s => ({
20
+ sessions: own.map(s => ({
16
21
  sessionId: s.sessionId,
17
22
  lastActivity: s.lastActivity,
18
23
  idleMinutes: s.idleMinutes,
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.17",
4
+ "version": "0.22.18",
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/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/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/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/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",
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/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/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/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",
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",