actual-mcp-server 0.8.10 → 0.8.13
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
|
@@ -628,6 +628,11 @@ OIDC_RESOURCE=your-client-id # must match 'aud' JWT claim
|
|
|
628
628
|
OIDC_SCOPES= # leave empty for Casdoor
|
|
629
629
|
```
|
|
630
630
|
|
|
631
|
+
**OAuth discovery endpoints (automatic in OIDC mode).** When `AUTH_PROVIDER=oidc`, the server publishes the two metadata documents an OAuth client needs to bootstrap a login, so `mcp-remote` and Claude.ai's native connector can discover the flow without manual endpoint configuration:
|
|
632
|
+
|
|
633
|
+
- `GET /.well-known/oauth-protected-resource` (RFC 9728): identifies this server as a protected resource and points at your `OIDC_ISSUER` as the authorization server.
|
|
634
|
+
- `GET /.well-known/oauth-authorization-server` (RFC 8414, #285): the authorization server metadata (its `authorization_endpoint` / `token_endpoint` / `registration_endpoint`), re-served from your IdP's own OpenID discovery document. This is here because several clients look for it on the resource-server origin, and some IdPs (e.g. Authentik) do not expose it where those clients look. It is fetched once at startup and served verbatim, exposes only endpoints your IdP already publishes publicly, and requires no authentication (a client reads it before it has a token). No extra configuration is needed; it is absent when `AUTH_PROVIDER` is not `oidc`.
|
|
635
|
+
|
|
631
636
|
See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) for `AUTH_BUDGET_ACL` format and Casdoor notes.
|
|
632
637
|
|
|
633
638
|
---
|
|
@@ -722,4 +727,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
722
727
|
|
|
723
728
|
---
|
|
724
729
|
|
|
725
|
-
**Version:** 0.8.
|
|
730
|
+
**Version:** 0.8.13 | **Tool Count:** 71 (verified LibreChat-compatible)
|
|
@@ -1361,14 +1361,46 @@ export async function mergePayees(targetId, mergeIds) {
|
|
|
1361
1361
|
await withConcurrency(() => retry(() => rawMergePayees(targetId, mergeIds), { retries: 0, backoffMs: 200 }));
|
|
1362
1362
|
});
|
|
1363
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Does a serialized Actual rule reference this payee? The payee id lives INSIDE a
|
|
1366
|
+
* condition or action whose `field` is 'payee': as `value` (op 'is'/'isNot') or as
|
|
1367
|
+
* a member of `value` (op 'oneOf', an array). A serialized rule has NO top-level
|
|
1368
|
+
* `payee_id` column, so the original BUG-3 post-filter (`r.payee_id === payeeId`)
|
|
1369
|
+
* matched nothing and made actual_payee_rules_get always return empty. This mirrors
|
|
1370
|
+
* `@actual-app/api`'s own getRulesForPayee, which scans payee-typed ids in the rule.
|
|
1371
|
+
*/
|
|
1372
|
+
export function ruleReferencesPayee(rule, payeeId) {
|
|
1373
|
+
const r = rule;
|
|
1374
|
+
const clauses = [
|
|
1375
|
+
...(Array.isArray(r?.conditions) ? r.conditions : []),
|
|
1376
|
+
...(Array.isArray(r?.actions) ? r.actions : []),
|
|
1377
|
+
];
|
|
1378
|
+
return clauses.some((c) => {
|
|
1379
|
+
if (c?.field !== 'payee')
|
|
1380
|
+
return false;
|
|
1381
|
+
return Array.isArray(c.value) ? c.value.includes(payeeId) : c?.value === payeeId;
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1364
1384
|
export async function getPayeeRules(payeeId) {
|
|
1365
1385
|
return withActualApi(async () => {
|
|
1366
1386
|
observability.incrementToolCall('actual.payees.getPayeeRules').catch(() => { });
|
|
1367
1387
|
const allRules = await withConcurrency(() => retry(() => rawGetPayeeRules(payeeId), { retries: 2, backoffMs: 200 }));
|
|
1368
1388
|
if (!Array.isArray(allRules))
|
|
1369
1389
|
return [];
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1390
|
+
// Defensive narrowing to the requested payee. @actual-app/api already scopes
|
|
1391
|
+
// payee-rules-get to the id; the filter must use the real rule shape (payee
|
|
1392
|
+
// referenced in a condition/action), NOT a nonexistent `payee_id` column.
|
|
1393
|
+
const filtered = allRules.filter((r) => ruleReferencesPayee(r, payeeId));
|
|
1394
|
+
// Upstream already scopes to the payee, so this filter should be a no-op. If it
|
|
1395
|
+
// ever drops rows, the serialized rule shape has drifted and the predicate is now
|
|
1396
|
+
// silently under-matching: exactly the failure class that made #284 return empty
|
|
1397
|
+
// for months. Surface it instead of hiding it.
|
|
1398
|
+
if (filtered.length !== allRules.length) {
|
|
1399
|
+
logger.debug('[ADAPTER] getPayeeRules post-filter dropped rows; rule shape may have drifted', {
|
|
1400
|
+
payeeId, upstream: allRules.length, kept: filtered.length,
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return filtered;
|
|
1372
1404
|
});
|
|
1373
1405
|
}
|
|
1374
1406
|
export async function batchBudgetUpdates(fn) {
|
|
@@ -172,11 +172,21 @@ export function resolveJwksUri(discoveryDoc, issuer, allowInsecure = false, trus
|
|
|
172
172
|
return jwks.toString();
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
|
-
* Fetch the issuer's OpenID discovery document and return
|
|
176
|
-
*
|
|
177
|
-
* missing/invalid/cross-host
|
|
175
|
+
* Fetch the issuer's OpenID discovery document ONCE and return both the raw
|
|
176
|
+
* document and its validated `jwks_uri` (#285). Fails closed on any problem
|
|
177
|
+
* (non-https issuer, redirect, non-200, bad JSON, missing/invalid/cross-host
|
|
178
|
+
* jwks_uri). `fetchImpl` is injectable for testing.
|
|
179
|
+
*
|
|
180
|
+
* The returned `metadata` is the IdP's own public discovery document. Its
|
|
181
|
+
* intended second use (beyond resolving the JWKS URI) is to be re-served
|
|
182
|
+
* verbatim at `/.well-known/oauth-authorization-server` as RFC 8414 Authorization
|
|
183
|
+
* Server Metadata, so a client that resolves that well-known path against this
|
|
184
|
+
* resource server (as mcp-remote and Claude.ai do) can find the IdP's
|
|
185
|
+
* `token_endpoint`. Because this fetch happens ONCE at startup and the document
|
|
186
|
+
* is served from memory, no request ever triggers an outbound fetch (no
|
|
187
|
+
* request-path SSRF surface).
|
|
178
188
|
*/
|
|
179
|
-
export async function
|
|
189
|
+
export async function discoverOidcMetadata(issuer, allowInsecure = false, trustedHosts = [], fetchImpl = fetch) {
|
|
180
190
|
assertSecureIssuer(issuer, allowInsecure);
|
|
181
191
|
const base = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
|
|
182
192
|
const discoveryUrl = `${base}/.well-known/openid-configuration`;
|
|
@@ -200,7 +210,20 @@ export async function discoverJwksUri(issuer, allowInsecure = false, trustedHost
|
|
|
200
210
|
catch {
|
|
201
211
|
throw new Error(`OIDC discovery document at ${discoveryUrl} is not valid JSON`);
|
|
202
212
|
}
|
|
213
|
+
// resolveJwksUri validates that `doc` is an object AND resolves/validates the
|
|
214
|
+
// jwks_uri (https, same-origin or explicitly allowlisted). It throws (fail
|
|
215
|
+
// closed) on any problem, so reaching past it guarantees a usable object with a
|
|
216
|
+
// trusted jwks_uri before the document is exposed.
|
|
203
217
|
const jwksUri = resolveJwksUri(doc, issuer, allowInsecure, trustedHosts);
|
|
204
218
|
logger.info('Resolved JWKS URI from OIDC discovery', { jwksUri });
|
|
219
|
+
return { jwksUri, metadata: doc };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Back-compat convenience: resolve only the validated `jwks_uri`. Delegates to
|
|
223
|
+
* discoverOidcMetadata so the single hardened fetch and every fail-closed check
|
|
224
|
+
* are shared. Retained for callers (and their tests) that only need the key set.
|
|
225
|
+
*/
|
|
226
|
+
export async function discoverJwksUri(issuer, allowInsecure = false, trustedHosts = [], fetchImpl = fetch) {
|
|
227
|
+
const { jwksUri } = await discoverOidcMetadata(issuer, allowInsecure, trustedHosts, fetchImpl);
|
|
205
228
|
return jwksUri;
|
|
206
229
|
}
|
|
@@ -13,7 +13,7 @@ import config from '../config.js';
|
|
|
13
13
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
14
14
|
import { MCPAuthTokenVerificationError } from 'mcp-auth';
|
|
15
15
|
import { createMcpAuth } from '../auth/setup.js';
|
|
16
|
-
import {
|
|
16
|
+
import { discoverOidcMetadata, buildTrustedJwksHosts } from '../lib/oidc-discovery.js';
|
|
17
17
|
import { buildAcceptedAudiences } from '../lib/oidc-audiences.js';
|
|
18
18
|
import { budgetAclMiddleware } from '../auth/budget-acl.js';
|
|
19
19
|
import * as https from 'node:https';
|
|
@@ -86,7 +86,24 @@ advertisedUrl) {
|
|
|
86
86
|
// #254: opt-in cross-origin JWKS trusted hosts (e.g. Google serves keys from
|
|
87
87
|
// www.googleapis.com). Parsed fail-fast here at the composition root and
|
|
88
88
|
// threaded as a parameter; empty (the default) keeps same-origin-only.
|
|
89
|
-
const jwksUri = await
|
|
89
|
+
const { jwksUri, metadata: oidcMetadata } = await discoverOidcMetadata(config.OIDC_ISSUER, config.OIDC_ALLOW_INSECURE_ISSUER, buildTrustedJwksHosts(config.OIDC_JWKS_TRUSTED_HOSTS));
|
|
90
|
+
// #285: serve RFC 8414 OAuth Authorization Server Metadata at this origin so
|
|
91
|
+
// mcp-remote and Claude.ai's native OAuth connector can discover the IdP's
|
|
92
|
+
// token_endpoint. We already serve RFC 9728 protected-resource metadata (whose
|
|
93
|
+
// authorization_servers points at the IdP), but several clients resolve the
|
|
94
|
+
// oauth-authorization-server well-known path against the RESOURCE server root,
|
|
95
|
+
// and some IdPs (e.g. Authentik) do not expose it where those clients look.
|
|
96
|
+
// The document is the IdP's own public discovery doc, fetched ONCE above (so no
|
|
97
|
+
// request-path fetch and no SSRF surface) and re-served verbatim as a BARE JSON
|
|
98
|
+
// object. Never emit a {jsonrpc,result} envelope on a well-known metadata path:
|
|
99
|
+
// that shape is not RFC-compliant and would break client parsing (the server-info
|
|
100
|
+
// probe was moved off this reserved path to /mcp-info as a bare object in #286).
|
|
101
|
+
// Unauthenticated by design: a client reads this before it holds a token, so it is
|
|
102
|
+
// registered here, before the bearer-auth middleware, and only when
|
|
103
|
+
// AUTH_PROVIDER=oidc (this block).
|
|
104
|
+
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
|
|
105
|
+
res.json(oidcMetadata);
|
|
106
|
+
});
|
|
90
107
|
const jwks = createRemoteJWKSet(new URL(jwksUri));
|
|
91
108
|
const customJwtVerify = async (token) => {
|
|
92
109
|
// Enforce the audience claim (#160, OWASP A07). Without it, any
|
|
@@ -568,34 +585,26 @@ advertisedUrl) {
|
|
|
568
585
|
}
|
|
569
586
|
await transport.handleRequest(req, res);
|
|
570
587
|
});
|
|
571
|
-
//
|
|
588
|
+
// Quick GET server-info probe used by our test harness (testMcpClient.ts, the
|
|
589
|
+
// Playwright E2E) to check the server is up and advertises tools before the full
|
|
590
|
+
// JSON-RPC handshake. #286: this used to live on /.well-known/oauth-protected-resource
|
|
591
|
+
// wrapped in a {jsonrpc,result} envelope, which squatted the RFC 9728 reserved OAuth
|
|
592
|
+
// path with non-metadata (a strict OAuth client probing it got garbage). It now lives
|
|
593
|
+
// on the non-reserved /mcp-info as a BARE object. The reserved OAuth path is therefore
|
|
594
|
+
// unregistered in non-OIDC mode (it correctly 404s: a static-bearer server has no
|
|
595
|
+
// authorization server to advertise), and in OIDC mode mcpAuth.protectedResourceMetadataRouter()
|
|
596
|
+
// (registered above) still serves the compliant RFC 9728 document.
|
|
572
597
|
const serverIp = process.env.MCP_BRIDGE_PUBLIC_HOST || getLocalIp();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
|
|
581
|
-
tools: toolsList,
|
|
582
|
-
advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
|
|
583
|
-
},
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
app.get('/.well-known/oauth-protected-resource/http', (_req, res) => {
|
|
587
|
-
res.json({
|
|
588
|
-
jsonrpc: '2.0',
|
|
589
|
-
result: {
|
|
590
|
-
description: serverDescription || "Actual MCP server",
|
|
591
|
-
instructions: serverInstructions || "Welcome to the Actual MCP server.",
|
|
592
|
-
serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
|
|
593
|
-
capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
|
|
594
|
-
tools: toolsList,
|
|
595
|
-
advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
+
const mcpInfo = () => ({
|
|
599
|
+
description: serverDescription || "Actual MCP server",
|
|
600
|
+
instructions: serverInstructions || "Welcome to the Actual MCP server.",
|
|
601
|
+
serverInstructions: { instructions: serverInstructions || "Welcome to the Actual MCP server." },
|
|
602
|
+
capabilities: capabilities && Object.keys(capabilities).length ? capabilities : { tools: toolsList.reduce((a, n) => ({ ...a, [n]: {} }), {}) },
|
|
603
|
+
tools: toolsList,
|
|
604
|
+
advertisedUrl: advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`,
|
|
598
605
|
});
|
|
606
|
+
app.get('/mcp-info', (_req, res) => { res.json(mcpInfo()); });
|
|
607
|
+
app.get('/mcp-info/http', (_req, res) => { res.json(mcpInfo()); });
|
|
599
608
|
app.get('/health', (_req, res) => {
|
|
600
609
|
const state = getConnectionState();
|
|
601
610
|
const poolStats = state.connectionPool || null;
|
|
@@ -4,8 +4,10 @@ export async function testMcpClient(advertisedUrl, _port, httpPath) {
|
|
|
4
4
|
const fetchAny = globalFetch ?? (await import('node-fetch')).default;
|
|
5
5
|
const fetchFn = fetchAny;
|
|
6
6
|
const base = new URL(advertisedUrl);
|
|
7
|
-
// quick probe endpoint
|
|
8
|
-
|
|
7
|
+
// quick probe endpoint (#286: the server-info probe moved off the reserved
|
|
8
|
+
// /.well-known/oauth-protected-resource OAuth path to the non-reserved /mcp-info,
|
|
9
|
+
// now a bare object; the `result ?? raw` unwrap below tolerates both shapes).
|
|
10
|
+
const probeUrl = `${base.origin}/mcp-info`;
|
|
9
11
|
console.info(`MCP client test: probing ${probeUrl}`);
|
|
10
12
|
const probeRes = await fetchFn(probeUrl, { method: 'GET' });
|
|
11
13
|
if (!probeRes.ok) {
|
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.8.
|
|
4
|
+
"version": "0.8.13",
|
|
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/generated_tools.smoke.test.js && node tests/unit/schema_validation.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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && 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/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",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_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/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.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/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/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && 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/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/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",
|
|
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",
|