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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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 = [...
|
|
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:
|
|
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
|
|
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:
|
|
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.
|
|
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",
|