actual-mcp-server 0.7.6 → 0.7.9
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 +20 -2
- package/dist/package.json +3 -3
- package/dist/src/config.js +5 -0
- package/dist/src/index.js +23 -3
- package/dist/src/lib/authPosture.js +91 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
[](https://modelcontextprotocol.io/)
|
|
13
13
|
[](https://hub.docker.com/r/agigante80/actual-mcp-server)
|
|
14
14
|
[](https://hub.docker.com/r/agigante80/actual-mcp-server)
|
|
15
|
+
[](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)
|
|
15
16
|
[](https://github.com/agigante80/actual-mcp-server/actions)
|
|
16
17
|
[](https://github.com/agigante80/actual-mcp-server)
|
|
17
18
|
|
|
@@ -55,7 +56,7 @@ Most Actual Budget MCP implementations are simple stdio bridges designed for sin
|
|
|
55
56
|
- [Available Tools](#available-tools)
|
|
56
57
|
- [Configuration](#configuration)
|
|
57
58
|
- [Multi-Budget Switching](#multi-budget-switching)
|
|
58
|
-
- [Transport & Authentication](#transport
|
|
59
|
+
- [Transport & Authentication](#transport--authentication)
|
|
59
60
|
- [Testing](#testing)
|
|
60
61
|
- [Documentation](#documentation)
|
|
61
62
|
- [Contributing](#contributing)
|
|
@@ -179,6 +180,22 @@ Add to `claude_desktop_config.json` (see [docs/guides/MCP_CLIENTS_SETUP.md](docs
|
|
|
179
180
|
>
|
|
180
181
|
> **`MCP_BRIDGE_DATA_DIR` should be an absolute path.** Without one, the data directory resolves relative to wherever the client spawns the process, which can be unpredictable. The directory is created automatically on first run.
|
|
181
182
|
|
|
183
|
+
### Option E: Unraid (Community Applications)
|
|
184
|
+
|
|
185
|
+
[](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)
|
|
186
|
+
|
|
187
|
+
Actual MCP Server is published in the Unraid **Community Applications** store: **[ca.unraid.net/apps/actual-mcp-server](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)**. This runs the HTTP transport, the right choice for LibreChat, LobeChat, and other remote MCP clients.
|
|
188
|
+
|
|
189
|
+
Install it from the **Apps** tab (Community Applications):
|
|
190
|
+
|
|
191
|
+
1. Open the **Apps** tab and search for **`actual-mcp-server`**, then click **Install**.
|
|
192
|
+
2. Fill in **Actual server URL**, **Actual server password**, and **Actual server Sync ID** (the Sync ID is in Actual Budget: open the budget, **Settings, Show advanced settings, Sync ID**).
|
|
193
|
+
3. **Set a strong MCP auth token.** Generate one with `openssl rand -hex 32`. A blank token disables all HTTP authentication and exposes your financial data unauthenticated on the LAN, so this is required (see [Transport & Authentication](#transport--authentication)).
|
|
194
|
+
4. Leave **PUID=99** and **PGID=100** (`nobody:users`) so the container can write the appdata Data and Logs directories, then start it.
|
|
195
|
+
5. Reach the health endpoint via the container's **WebUI** link (port `3600`); point your MCP client at `http://[server-ip]:3600/http` with the Bearer token.
|
|
196
|
+
|
|
197
|
+
The Unraid template lives in [`unraid/actual-mcp-server.xml`](unraid/actual-mcp-server.xml). For the publishing workflow see [docs/UNRAID_CA_PUBLISHING.md](docs/UNRAID_CA_PUBLISHING.md).
|
|
198
|
+
|
|
182
199
|
### Connect an AI client
|
|
183
200
|
|
|
184
201
|
**LibreChat / LobeChat**: add to `librechat.yaml` (or LobeChat MCP plugin settings):
|
|
@@ -451,6 +468,7 @@ All configuration is via environment variables. Copy `.env.example` to `.env` to
|
|
|
451
468
|
| **Security & Authentication** ||||
|
|
452
469
|
| `AUTH_PROVIDER` | `none` | No | Auth mode: `none` (static Bearer) or `oidc` (JWKS-validated JWT) |
|
|
453
470
|
| `MCP_SSE_AUTHORIZATION` | _(none)_ | No | Static Bearer token (`AUTH_PROVIDER=none`; highly recommended in production) |
|
|
471
|
+
| `MCP_ALLOW_UNAUTHENTICATED` | `false` | No | Opt-out for required-by-default HTTP auth (#242). On a non-loopback bind with no token and no OIDC the server refuses to start; set to `true` to run open deliberately (e.g. behind your own proxy) |
|
|
454
472
|
| `OIDC_ISSUER` | _(none)_ | If OIDC | OIDC issuer URL (e.g., `https://sso.example.com`) |
|
|
455
473
|
| `OIDC_RESOURCE` | _(none)_ | No | Expected `aud` claim in JWT (your client ID) |
|
|
456
474
|
| `OIDC_SCOPES` | _(none)_ | No | Comma-separated required scopes; leave empty for Casdoor |
|
|
@@ -698,4 +716,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
698
716
|
|
|
699
717
|
---
|
|
700
718
|
|
|
701
|
-
**Version:** 0.7.
|
|
719
|
+
**Version:** 0.7.9 | **Tool Count:** 71 (verified LibreChat-compatible)
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.9",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,14 +30,14 @@
|
|
|
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/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/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",
|
|
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/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_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",
|
|
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",
|
|
37
37
|
"test:e2e:docker:smoke": "./tests/e2e/run-docker-e2e.sh smoke",
|
|
38
38
|
"test:e2e:docker:full": "./tests/e2e/run-docker-e2e.sh full",
|
|
39
39
|
"test:all": "npm run test:adapter && npm run test:unit-js && npm run test:e2e:docker:smoke",
|
|
40
|
-
"test:mcp-client": "npm run build && node scripts/register-tsconfig-paths.js -- --http --test-mcp-client",
|
|
40
|
+
"test:mcp-client": "npm run build && MCP_ALLOW_UNAUTHENTICATED=true node scripts/register-tsconfig-paths.js -- --http --test-mcp-client",
|
|
41
41
|
"test:integration": "MCP_TEST_LEVEL=sanity node tests/manual/index.js",
|
|
42
42
|
"test:integration:sanity": "MCP_TEST_LEVEL=sanity node tests/manual/index.js",
|
|
43
43
|
"test:integration:smoke": "MCP_TEST_LEVEL=smoke node tests/manual/index.js",
|
package/dist/src/config.js
CHANGED
|
@@ -13,6 +13,11 @@ export const configSchema = z.object({
|
|
|
13
13
|
MCP_BRIDGE_PORT: z.string().default('3600'),
|
|
14
14
|
MCP_TRANSPORT_MODE: z.enum(['--http']).default('--http'),
|
|
15
15
|
MCP_SSE_AUTHORIZATION: z.string().optional(),
|
|
16
|
+
// #242: explicit opt-out for the required-by-default HTTP auth gate. When auth
|
|
17
|
+
// is unconfigured AND the bind is non-loopback, the server refuses to start
|
|
18
|
+
// unless this is the exact string 'true'. Strict parse (only 'true' enables)
|
|
19
|
+
// so a typo like 'yes'/'1' cannot silently leave the server open.
|
|
20
|
+
MCP_ALLOW_UNAUTHENTICATED: z.string().optional().transform(val => val === 'true'),
|
|
16
21
|
MCP_ENABLE_HTTPS: z.string().optional().transform(val => val === 'true'),
|
|
17
22
|
MCP_HTTPS_CERT: z.string().optional(),
|
|
18
23
|
MCP_HTTPS_KEY: z.string().optional(),
|
package/dist/src/index.js
CHANGED
|
@@ -124,9 +124,10 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
124
124
|
import('./actualConnection.js'),
|
|
125
125
|
import('./lib/ActualMCPConnection.js'),
|
|
126
126
|
]);
|
|
127
|
-
const [{ startHttpServer }, { startStdioServer }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
127
|
+
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
128
128
|
import('./server/httpServer.js'),
|
|
129
129
|
import('./server/stdioServer.js'),
|
|
130
|
+
import('./lib/authPosture.js'),
|
|
130
131
|
import('./logger.js'),
|
|
131
132
|
import('./utils.js'),
|
|
132
133
|
import('./actualToolsManager.js'),
|
|
@@ -242,12 +243,31 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
}
|
|
246
|
+
// #242: the interface both the self-test and the normal HTTP path bind to.
|
|
247
|
+
const bindHost = process.env.MCP_BRIDGE_BIND_HOST || '0.0.0.0';
|
|
248
|
+
// #242: HTTP auth required-by-default. Decide ONCE, before binding, whether
|
|
249
|
+
// the configured posture is safe. On a non-loopback bind with no auth and no
|
|
250
|
+
// explicit opt-out this refuses startup (exit 1) rather than silently serving
|
|
251
|
+
// financial data open on the LAN. stdio mode never reaches this check.
|
|
252
|
+
if (useHttp || useTestMcpClient) {
|
|
253
|
+
const decision = enforceHttpAuthPosture({
|
|
254
|
+
bindHost,
|
|
255
|
+
hasStaticToken: !!process.env.MCP_SSE_AUTHORIZATION,
|
|
256
|
+
oidcEnabled: process.env.AUTH_PROVIDER === 'oidc',
|
|
257
|
+
allowUnauthenticated: process.env.MCP_ALLOW_UNAUTHENTICATED === 'true',
|
|
258
|
+
});
|
|
259
|
+
// enforceHttpAuthPosture already called process.exit(1) on 'refuse'. This
|
|
260
|
+
// explicit return makes the open-start path structurally unreachable even
|
|
261
|
+
// if process.exit were ever non-terminal, so we never bind on a refuse.
|
|
262
|
+
if (decision === 'refuse')
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
245
265
|
// If requested, run the MCP client-side tests now that all variables are ready
|
|
246
266
|
if (useTestMcpClient) {
|
|
247
267
|
logger.info('⚙️ --test-mcp-client specified, starting HTTP server and running client-side MCP tests...');
|
|
248
268
|
// start server in http mode. Bind all interfaces by default (#239): advertisedUrl
|
|
249
269
|
// uses the LAN IP (getLocalIp), so the self-test client must be able to reach it.
|
|
250
|
-
await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version,
|
|
270
|
+
await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version, bindHost, advertisedUrl);
|
|
251
271
|
// dynamic import to avoid circular at top
|
|
252
272
|
const { testMcpClient } = await import('./tests/testMcpClient.js');
|
|
253
273
|
try {
|
|
@@ -284,7 +304,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
284
304
|
logger.info('Mode: HTTP');
|
|
285
305
|
await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version,
|
|
286
306
|
// bind host (ensure server accepts connections on that interface)
|
|
287
|
-
|
|
307
|
+
bindHost,
|
|
288
308
|
// advertised URL shown to clients
|
|
289
309
|
advertisedUrl);
|
|
290
310
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createModuleLogger } from './loggerFactory.js';
|
|
2
|
+
/**
|
|
3
|
+
* Auth posture gate (#242): make HTTP authentication required-by-default.
|
|
4
|
+
*
|
|
5
|
+
* Background: with AUTH_PROVIDER unset and a blank MCP_SSE_AUTHORIZATION the
|
|
6
|
+
* per-request gate in httpServer.ts serves every request, and the server binds
|
|
7
|
+
* 0.0.0.0 by default, so a forgotten token publishes a financial MCP server open
|
|
8
|
+
* on the LAN. This module decides ONCE at startup whether that posture is safe,
|
|
9
|
+
* before the socket is bound.
|
|
10
|
+
*
|
|
11
|
+
* We refuse startup (Option 1) rather than 401 per request (Option 2): a
|
|
12
|
+
* per-request loopback check would key on req.ip, which is attacker-suppliable
|
|
13
|
+
* via X-Forwarded-For once `trust proxy` is enabled behind the documented reverse
|
|
14
|
+
* proxy, turning the check into a spoofable bypass. The bind host is fixed config
|
|
15
|
+
* no client can influence, so deciding once at startup is the sound boundary.
|
|
16
|
+
*/
|
|
17
|
+
const logger = createModuleLogger('AUTH_POSTURE');
|
|
18
|
+
/**
|
|
19
|
+
* True only for binds that never leave the local host: `localhost`, the entire
|
|
20
|
+
* IPv4 loopback block (127.0.0.0/8, so 127.0.0.2 counts, not just 127.0.0.1),
|
|
21
|
+
* IPv6 loopback in compressed and longhand form, and the IPv4-mapped IPv6
|
|
22
|
+
* loopback. `0.0.0.0` and `::` bind ALL interfaces (including non-loopback ones)
|
|
23
|
+
* so they are exposed, not loopback. An empty or undefined host is treated as
|
|
24
|
+
* exposed (fail-safe): the real runtime default is `0.0.0.0`.
|
|
25
|
+
*/
|
|
26
|
+
export function isLoopbackHost(host) {
|
|
27
|
+
if (!host)
|
|
28
|
+
return false;
|
|
29
|
+
const h = host.trim().toLowerCase();
|
|
30
|
+
if (h === 'localhost')
|
|
31
|
+
return true;
|
|
32
|
+
// IPv4 loopback is the whole 127.0.0.0/8 block, not only 127.0.0.1.
|
|
33
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h))
|
|
34
|
+
return true;
|
|
35
|
+
// IPv6 loopback: compressed (::1) and fully-expanded longhand.
|
|
36
|
+
if (h === '::1' || h === '0:0:0:0:0:0:0:1' || h === '0000:0000:0000:0000:0000:0000:0000:0001')
|
|
37
|
+
return true;
|
|
38
|
+
// IPv4-mapped IPv6 loopback (e.g. ::ffff:127.0.0.1).
|
|
39
|
+
if (/^::ffff:127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h))
|
|
40
|
+
return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pure decision function (no side effects) so it is exhaustively unit-testable
|
|
45
|
+
* without booting a server. Precedence: configured auth serves; otherwise a
|
|
46
|
+
* loopback bind serves; otherwise the explicit opt-out serves (with a warning);
|
|
47
|
+
* otherwise refuse.
|
|
48
|
+
*/
|
|
49
|
+
export function evaluateHttpAuthPosture(input) {
|
|
50
|
+
const { bindHost, hasStaticToken, oidcEnabled, allowUnauthenticated } = input;
|
|
51
|
+
if (hasStaticToken || oidcEnabled) {
|
|
52
|
+
return { decision: 'serve', reason: 'authentication is configured' };
|
|
53
|
+
}
|
|
54
|
+
if (isLoopbackHost(bindHost)) {
|
|
55
|
+
return { decision: 'serve', reason: `loopback bind (${bindHost}) is not exposed` };
|
|
56
|
+
}
|
|
57
|
+
if (allowUnauthenticated) {
|
|
58
|
+
return {
|
|
59
|
+
decision: 'serve-open-warn',
|
|
60
|
+
reason: `MCP_ALLOW_UNAUTHENTICATED=true on a non-loopback bind (${bindHost ?? '0.0.0.0'})`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
decision: 'refuse',
|
|
65
|
+
reason: `no authentication configured on a non-loopback bind (${bindHost ?? '0.0.0.0'})`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Evaluate the posture and enforce it: refuse exits the process non-zero with a
|
|
70
|
+
* remediation message, the opt-out logs a loud warning, and a safe posture is a
|
|
71
|
+
* no-op. `exit` and `log` are injectable only for the existing process.exit-spy
|
|
72
|
+
* test pattern; production callers use the defaults. Returns the decision so a
|
|
73
|
+
* caller (or test that stubs exit to not throw) can observe it.
|
|
74
|
+
*/
|
|
75
|
+
export function enforceHttpAuthPosture(input, log = logger, exit = process.exit) {
|
|
76
|
+
const { decision, reason } = evaluateHttpAuthPosture(input);
|
|
77
|
+
if (decision === 'refuse') {
|
|
78
|
+
log.error(`Refusing to start: ${reason}. HTTP auth is required-by-default. ` +
|
|
79
|
+
'Set MCP_SSE_AUTHORIZATION to a strong token (openssl rand -hex 32), or ' +
|
|
80
|
+
'set AUTH_PROVIDER=oidc, or bind a loopback host. To deliberately run open ' +
|
|
81
|
+
'(for example behind your own authenticating proxy), set MCP_ALLOW_UNAUTHENTICATED=true.');
|
|
82
|
+
exit(1);
|
|
83
|
+
}
|
|
84
|
+
else if (decision === 'serve-open-warn') {
|
|
85
|
+
log.warn(`Serving HTTP WITHOUT authentication: ${reason}. Anyone who can reach this ` +
|
|
86
|
+
'port can read and modify your financial data. This is allowed only because ' +
|
|
87
|
+
'MCP_ALLOW_UNAUTHENTICATED=true. Put an authenticating reverse proxy in front, ' +
|
|
88
|
+
'or set MCP_SSE_AUTHORIZATION / AUTH_PROVIDER=oidc.');
|
|
89
|
+
}
|
|
90
|
+
return decision;
|
|
91
|
+
}
|
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.7.
|
|
4
|
+
"version": "0.7.9",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -30,14 +30,14 @@
|
|
|
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/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/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",
|
|
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/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_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",
|
|
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",
|
|
37
37
|
"test:e2e:docker:smoke": "./tests/e2e/run-docker-e2e.sh smoke",
|
|
38
38
|
"test:e2e:docker:full": "./tests/e2e/run-docker-e2e.sh full",
|
|
39
39
|
"test:all": "npm run test:adapter && npm run test:unit-js && npm run test:e2e:docker:smoke",
|
|
40
|
-
"test:mcp-client": "npm run build && node scripts/register-tsconfig-paths.js -- --http --test-mcp-client",
|
|
40
|
+
"test:mcp-client": "npm run build && MCP_ALLOW_UNAUTHENTICATED=true node scripts/register-tsconfig-paths.js -- --http --test-mcp-client",
|
|
41
41
|
"test:integration": "MCP_TEST_LEVEL=sanity node tests/manual/index.js",
|
|
42
42
|
"test:integration:sanity": "MCP_TEST_LEVEL=sanity node tests/manual/index.js",
|
|
43
43
|
"test:integration:smoke": "MCP_TEST_LEVEL=smoke node tests/manual/index.js",
|