actual-mcp-server 0.24.15 → 0.25.1
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 +2 -1
- package/dist/src/config.js +29 -0
- package/dist/src/index.js +5 -2
- package/dist/src/lib/authPosture.js +133 -5
- package/dist/src/server/httpServer.js +74 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server that lets an
|
|
|
21
21
|
|
|
22
22
|
- **Claude Desktop, Claude Code, Cursor, VS Code or another local client:** run it over stdio with `npx`, no server to keep running. Follow your client's page in [docs/guides/clients/](docs/guides/clients/README.md), for example [Claude Desktop](docs/guides/clients/claude-desktop.md) or [Claude Code](docs/guides/clients/claude-code.md).
|
|
23
23
|
- **A chat UI such as LibreChat or LobeChat, or several users:** run it over HTTP in Docker with the quick start below, then follow [LibreChat](docs/guides/clients/librechat.md) or [LobeChat](docs/guides/clients/lobechat.md).
|
|
24
|
+
- **A client that installs from the official [MCP Registry](https://registry.modelcontextprotocol.io/):** it is listed as `io.github.agigante80/actual-mcp-server`, with the npm package (stdio) and the Docker image (HTTP); the client asks for the same settings as the pages above.
|
|
24
25
|
- **Unraid:** install it from [Community Applications](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg); see [Unraid](docs/guides/deployment.md#unraid-and-puidpgid-227).
|
|
25
26
|
|
|
26
27
|
## Quick start (Docker, HTTP)
|
|
@@ -168,4 +169,4 @@ MIT, see [LICENSE](LICENSE). Built on [Actual Budget](https://actualbudget.org/)
|
|
|
168
169
|
|
|
169
170
|
---
|
|
170
171
|
|
|
171
|
-
**Version:** 0.
|
|
172
|
+
**Version:** 0.25.1 | **Tool Count:** 84 (verified LibreChat-compatible)
|
package/dist/src/config.js
CHANGED
|
@@ -2,6 +2,10 @@ import { z } from 'zod';
|
|
|
2
2
|
import { parseIdentityMap } from './auth/identity-map.js';
|
|
3
3
|
import { parseBudgetAcl } from './auth/budget-acl-parse.js';
|
|
4
4
|
import { parseStaticTokensConfig, staticTokensConfigured } from './auth/static-tokens.js';
|
|
5
|
+
/** #612: MCP_ALLOWED_HOSTS as a list: comma-separated, trimmed, lowercased, empties dropped. */
|
|
6
|
+
export function parseAllowedHostsList(value) {
|
|
7
|
+
return (value ?? '').split(',').map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
8
|
+
}
|
|
5
9
|
// #466: the byte count body-parser will ACTUALLY enforce for MCP_HTTP_BODY_LIMIT, or
|
|
6
10
|
// null when the value must be refused. Mirrors `bytes.parse` (what body-parser uses)
|
|
7
11
|
// without importing that transitive package: floor(number * unit), units b and kb
|
|
@@ -126,6 +130,31 @@ export const configSchema = z.object({
|
|
|
126
130
|
// so a typo like 'yes'/'1' cannot silently leave the server open.
|
|
127
131
|
MCP_ALLOW_UNAUTHENTICATED: z.string().optional().transform(val => val === 'true'),
|
|
128
132
|
MCP_ENABLE_HTTPS: z.string().optional().transform(val => val === 'true'),
|
|
133
|
+
// #612: extra hostnames the HTTP server accepts as Host (and as Origin in open
|
|
134
|
+
// mode), comma-separated, additive to localhost, 127.0.0.1 and [::1]. Each entry
|
|
135
|
+
// must already be in the form new URL() normalises a hostname to: no port, path,
|
|
136
|
+
// scheme or wildcard, IPv6 bracketed, IPv4 dotted. Parsed to a lowercased array.
|
|
137
|
+
MCP_ALLOWED_HOSTS: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.superRefine((v, ctx) => {
|
|
141
|
+
for (const entry of parseAllowedHostsList(v)) {
|
|
142
|
+
let normalised = null;
|
|
143
|
+
try {
|
|
144
|
+
normalised = new URL(`http://${entry}`).hostname;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// reported below
|
|
148
|
+
}
|
|
149
|
+
if (entry.includes('*') || normalised !== entry) {
|
|
150
|
+
ctx.addIssue({
|
|
151
|
+
code: 'custom',
|
|
152
|
+
message: `MCP_ALLOWED_HOSTS entry "${entry}" is not a bare hostname (no port, path, scheme or wildcard; IPv6 in brackets)`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.transform(parseAllowedHostsList),
|
|
129
158
|
MCP_HTTPS_CERT: z.string().optional(),
|
|
130
159
|
MCP_HTTPS_KEY: z.string().optional(),
|
|
131
160
|
// Explicit cap on incoming JSON request bodies (#168). Passed to
|
package/dist/src/index.js
CHANGED
|
@@ -191,7 +191,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
191
191
|
import('./actualConnection.js'),
|
|
192
192
|
import('./lib/ActualMCPConnection.js'),
|
|
193
193
|
]);
|
|
194
|
-
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
194
|
+
const [{ startHttpServer }, { startStdioServer }, { enforceHttpAuthPosture, findWeakTokenVars }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
195
195
|
import('./server/httpServer.js'),
|
|
196
196
|
import('./server/stdioServer.js'),
|
|
197
197
|
import('./lib/authPosture.js'),
|
|
@@ -301,13 +301,16 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
301
301
|
// explicit opt-out this refuses startup (exit 1) rather than silently serving
|
|
302
302
|
// financial data open on the LAN. stdio mode never reaches this check.
|
|
303
303
|
if (useHttp || useTestMcpClient) {
|
|
304
|
+
const oidcEnabled = process.env.AUTH_PROVIDER === 'oidc';
|
|
304
305
|
const decision = enforceHttpAuthPosture({
|
|
305
306
|
bindHost,
|
|
306
307
|
// #572: MCP_STATIC_TOKENS is authentication too. A set-but-unresolvable value
|
|
307
308
|
// still counts here: startHttpServer refuses it before binding.
|
|
308
309
|
hasStaticToken: !!process.env.MCP_SSE_AUTHORIZATION || !!process.env.MCP_STATIC_TOKENS?.trim(),
|
|
309
|
-
oidcEnabled
|
|
310
|
+
oidcEnabled,
|
|
310
311
|
allowUnauthenticated: process.env.MCP_ALLOW_UNAUTHENTICATED === 'true',
|
|
312
|
+
// #611: a short or placeholder token refuses a non-loopback bind.
|
|
313
|
+
weakTokenVars: findWeakTokenVars(process.env, { oidcEnabled }),
|
|
311
314
|
});
|
|
312
315
|
// enforceHttpAuthPosture already called process.exit(1) on 'refuse'. This
|
|
313
316
|
// explicit return makes the open-start path structurally unreachable even
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createModuleLogger } from './loggerFactory.js';
|
|
2
|
+
import { MIN_TOKEN_LENGTH, parseStaticTokensConfig } from '../auth/static-tokens.js';
|
|
2
3
|
/**
|
|
3
4
|
* Auth posture gate (#242): make HTTP authentication required-by-default.
|
|
4
5
|
*
|
|
@@ -40,14 +41,124 @@ export function isLoopbackHost(host) {
|
|
|
40
41
|
return true;
|
|
41
42
|
return false;
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* #611: example values shipped in our docs and .env.example. A token equal to one
|
|
46
|
+
* of these (trimmed, case-insensitive) is weak whatever its length:
|
|
47
|
+
* `replace-with-openssl-rand-hex-32` is exactly 32 characters.
|
|
48
|
+
*/
|
|
49
|
+
const PLACEHOLDER_TOKENS = new Set(['your-secure-token-here', 'replace-with-openssl-rand-hex-32', 'changeme']);
|
|
50
|
+
function isPlaceholder(value) {
|
|
51
|
+
return PLACEHOLDER_TOKENS.has(value.trim().toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* #611: the names (never the values) of the configured bearer-token variables that
|
|
55
|
+
* are weak. Pure: reads only the `env` it is given.
|
|
56
|
+
*
|
|
57
|
+
* - Under OIDC nothing is checked: httpServer.ts builds no bearer entries there, so
|
|
58
|
+
* a stale MCP_SSE_AUTHORIZATION is not a credential.
|
|
59
|
+
* - MCP_SSE_AUTHORIZATION: weak when set, non-empty and shorter than
|
|
60
|
+
* MIN_TOKEN_LENGTH after trimming (whitespace-only included), or a placeholder.
|
|
61
|
+
* EMPTY stays the no-auth path (tests/e2e/compose.yaml runs open on it).
|
|
62
|
+
* - MCP_TOKEN_*: resolveStaticTokens already refuses a short value on any bind, so
|
|
63
|
+
* only placeholders are added here. An unparsable MCP_STATIC_TOKENS is left to
|
|
64
|
+
* resolveStaticTokens, which refuses it with its own message.
|
|
65
|
+
*/
|
|
66
|
+
export function findWeakTokenVars(env, { oidcEnabled }) {
|
|
67
|
+
if (oidcEnabled)
|
|
68
|
+
return [];
|
|
69
|
+
const weak = [];
|
|
70
|
+
const legacy = env.MCP_SSE_AUTHORIZATION;
|
|
71
|
+
if (legacy && (legacy.trim().length < MIN_TOKEN_LENGTH || isPlaceholder(legacy))) {
|
|
72
|
+
weak.push('MCP_SSE_AUTHORIZATION');
|
|
73
|
+
}
|
|
74
|
+
let principals = {};
|
|
75
|
+
try {
|
|
76
|
+
principals = parseStaticTokensConfig(env.MCP_STATIC_TOKENS);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Refused later by resolveStaticTokens, before bind.
|
|
80
|
+
}
|
|
81
|
+
for (const { tokenEnv } of Object.values(principals)) {
|
|
82
|
+
const value = env[tokenEnv];
|
|
83
|
+
if (value && isPlaceholder(value) && !weak.includes(tokenEnv))
|
|
84
|
+
weak.push(tokenEnv);
|
|
85
|
+
}
|
|
86
|
+
return weak;
|
|
87
|
+
}
|
|
88
|
+
/** #612: the hostnames every open-mode server accepts, in the SDK's bracketed form. */
|
|
89
|
+
const LOOPBACK_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]'];
|
|
90
|
+
/**
|
|
91
|
+
* The hostname the SDK compares for `host` (a bind address or advertised name):
|
|
92
|
+
* lowercased, IPv6 bracketed and canonical. `undefined` for an empty or wildcard
|
|
93
|
+
* value, which names no single host.
|
|
94
|
+
*/
|
|
95
|
+
function canonicalHostname(host) {
|
|
96
|
+
const h = host?.trim().toLowerCase();
|
|
97
|
+
if (!h || h === '0.0.0.0' || h === '::' || h === '[::]')
|
|
98
|
+
return undefined;
|
|
99
|
+
const bracketed = h.includes(':') && !h.startsWith('[') ? `[${h}]` : h;
|
|
100
|
+
// The SDK compares the URL-parsed Host, which is the canonical form
|
|
101
|
+
// ([2001:db8:0:0::5] arrives as [2001:db8::5]), so store the same form.
|
|
102
|
+
try {
|
|
103
|
+
return new URL(`http://${bracketed}`).hostname;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return bracketed;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* #612: the Host (and, in open mode, Origin) hostnames the HTTP server accepts, or
|
|
111
|
+
* `null` to skip the check. DNS rebinding protection: a hostile page can rebind its
|
|
112
|
+
* own name to 127.0.0.1, but cannot make the browser send a Host it does not own.
|
|
113
|
+
*
|
|
114
|
+
* - Open mode (no bearer token, no OIDC): the loopback names, `allowedHosts`, the
|
|
115
|
+
* bind address itself when it is a concrete, non-wildcard address (an IP-literal
|
|
116
|
+
* Host cannot come out of rebinding), and (#615) the advertised host, the address
|
|
117
|
+
* the server prints as its own URL (MCP_BRIDGE_PUBLIC_HOST or the detected LAN
|
|
118
|
+
* IP): an operator-controlled name or an IP literal, never one a hostile page can
|
|
119
|
+
* make the browser send. Not on a loopback bind, which the LAN cannot reach.
|
|
120
|
+
* IPv6 values are added bracketed and canonical.
|
|
121
|
+
* - Authenticated: `null` when `allowedHosts` is empty (unchanged behaviour), else
|
|
122
|
+
* `allowedHosts` plus the loopback names. The bind and advertised hosts are not
|
|
123
|
+
* added: an operator list stays exact.
|
|
124
|
+
*
|
|
125
|
+
* Pure: `allowedHosts` is the already-validated MCP_ALLOWED_HOSTS list.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveAllowedHosts({ openMode, allowedHosts, bindHost, advertisedHost, }) {
|
|
128
|
+
if (!openMode && allowedHosts.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
const hosts = new Set([...LOOPBACK_HOSTNAMES, ...allowedHosts]);
|
|
131
|
+
if (openMode) {
|
|
132
|
+
// A loopback bind is unreachable from the LAN, so the advertised (LAN) host is
|
|
133
|
+
// left out there: adding it would only widen which Origins may call localhost.
|
|
134
|
+
const advertised = isLoopbackHost(bindHost) ? undefined : advertisedHost;
|
|
135
|
+
for (const h of [canonicalHostname(bindHost), canonicalHostname(advertised)]) {
|
|
136
|
+
if (h)
|
|
137
|
+
hosts.add(h);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [...hosts];
|
|
141
|
+
}
|
|
43
142
|
/**
|
|
44
143
|
* Pure decision function (no side effects) so it is exhaustively unit-testable
|
|
45
|
-
* without booting a server. Precedence:
|
|
46
|
-
*
|
|
47
|
-
*
|
|
144
|
+
* without booting a server. Precedence: a weak token refuses a non-loopback bind
|
|
145
|
+
* (#611, before and regardless of the opt-out, which cannot make a weak token
|
|
146
|
+
* safe) and serves with a warning on loopback; then configured auth serves;
|
|
147
|
+
* otherwise a loopback bind serves; otherwise the explicit opt-out serves (with a
|
|
148
|
+
* warning); otherwise refuse.
|
|
48
149
|
*/
|
|
49
150
|
export function evaluateHttpAuthPosture(input) {
|
|
50
|
-
const { bindHost, hasStaticToken, oidcEnabled, allowUnauthenticated } = input;
|
|
151
|
+
const { bindHost, hasStaticToken, oidcEnabled, allowUnauthenticated, weakTokenVars = [] } = input;
|
|
152
|
+
if (weakTokenVars.length > 0) {
|
|
153
|
+
if (isLoopbackHost(bindHost)) {
|
|
154
|
+
return { decision: 'serve', reason: `loopback bind (${bindHost}) with a weak token`, weakTokenVars };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
decision: 'refuse',
|
|
158
|
+
reason: `weak token on a non-loopback bind (${bindHost ?? '0.0.0.0'})`,
|
|
159
|
+
weakTokenVars,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
51
162
|
if (hasStaticToken || oidcEnabled) {
|
|
52
163
|
return { decision: 'serve', reason: 'authentication is configured' };
|
|
53
164
|
}
|
|
@@ -73,7 +184,24 @@ export function evaluateHttpAuthPosture(input) {
|
|
|
73
184
|
* caller (or test that stubs exit to not throw) can observe it.
|
|
74
185
|
*/
|
|
75
186
|
export function enforceHttpAuthPosture(input, log = logger, exit = process.exit) {
|
|
76
|
-
const { decision, reason } = evaluateHttpAuthPosture(input);
|
|
187
|
+
const { decision, reason, weakTokenVars } = evaluateHttpAuthPosture(input);
|
|
188
|
+
if (weakTokenVars) {
|
|
189
|
+
// #611: names only, never values. A separate message from the no-auth refusal:
|
|
190
|
+
// MCP_ALLOW_UNAUTHENTICATED cannot rescue a weak token, so it is not offered.
|
|
191
|
+
if (decision === 'refuse') {
|
|
192
|
+
log.error(`Refusing to start: ${reason}. ${weakTokenVars.join(', ')} is shorter than ${MIN_TOKEN_LENGTH} ` +
|
|
193
|
+
'characters or an example placeholder. Generate a token with openssl rand -hex 32.');
|
|
194
|
+
exit(1);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
for (const name of weakTokenVars) {
|
|
198
|
+
log.warn(`${name} is shorter than ${MIN_TOKEN_LENGTH} characters or an example placeholder. ` +
|
|
199
|
+
'Allowed only on a loopback bind; a non-loopback bind refuses to start. ' +
|
|
200
|
+
'Generate a token with openssl rand -hex 32.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return decision;
|
|
204
|
+
}
|
|
77
205
|
if (decision === 'refuse') {
|
|
78
206
|
log.error(`Refusing to start: ${reason}. HTTP auth is required-by-default. ` +
|
|
79
207
|
'Set MCP_SSE_AUTHORIZATION to a strong token (openssl rand -hex 32), or set ' +
|
|
@@ -25,6 +25,8 @@ import { parseScopeList } from '../lib/oidc-scopes.js';
|
|
|
25
25
|
import { budgetAclMiddleware } from '../auth/budget-acl.js';
|
|
26
26
|
import { resolveStaticTokens, matchBearerToken, LEGACY_STATIC_PRINCIPAL } from '../auth/static-tokens.js';
|
|
27
27
|
import { runAclPreflight } from '../auth/budget-acl-dynamic.js';
|
|
28
|
+
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
|
|
29
|
+
import { resolveAllowedHosts } from '../lib/authPosture.js';
|
|
28
30
|
import * as https from 'node:https';
|
|
29
31
|
import { finished } from 'node:stream';
|
|
30
32
|
import * as fs from 'node:fs';
|
|
@@ -85,7 +87,78 @@ advertisedUrl) {
|
|
|
85
87
|
? []
|
|
86
88
|
: resolveStaticTokens(config.MCP_STATIC_TOKENS, process.env, getBudgetRegistry(), config.MCP_SSE_AUTHORIZATION);
|
|
87
89
|
const staticPrincipals = bearerEntries.filter((e) => e.allowedBudgets);
|
|
90
|
+
// #612: the ONE open-mode predicate, shared by the Host/Origin guard below and
|
|
91
|
+
// checkAuth's no-token branch, so the two can never disagree.
|
|
92
|
+
const openMode = config.AUTH_PROVIDER !== 'oidc' && bearerEntries.length === 0;
|
|
88
93
|
const app = express();
|
|
94
|
+
// #612: DNS rebinding protection, mounted first so it covers every route (/health,
|
|
95
|
+
// /metrics, /.well-known/* and OPTIONS included). It reads req.headers.host only,
|
|
96
|
+
// never X-Forwarded-Host or req.hostname, so a proxy header cannot bypass it.
|
|
97
|
+
// #615: the host this server prints as its own URL is accepted too (open mode only).
|
|
98
|
+
let advertisedHost;
|
|
99
|
+
try {
|
|
100
|
+
advertisedHost = advertisedUrl ? new URL(advertisedUrl).hostname : undefined;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
logger.warn(`[HTTP] Advertised URL ${advertisedUrl} does not parse; its host is not accepted. Check MCP_BRIDGE_PUBLIC_HOST (a bare host or IP, no scheme or port).`);
|
|
104
|
+
}
|
|
105
|
+
// MCP_BRIDGE_PUBLIC_HOST=https://nas.lan parses to the hostname "https": say so rather than
|
|
106
|
+
// accept it silently. A scheme or path always carries a "/"; a port already fails the parse.
|
|
107
|
+
const publicHost = process.env.MCP_BRIDGE_PUBLIC_HOST?.trim().toLowerCase();
|
|
108
|
+
if (advertisedHost && publicHost?.includes('/')) {
|
|
109
|
+
logger.warn(`[HTTP] MCP_BRIDGE_PUBLIC_HOST (${publicHost}) did not parse to a host name (got ${advertisedHost}). Set a bare host or IP, no scheme or port.`);
|
|
110
|
+
}
|
|
111
|
+
const allowedHosts = resolveAllowedHosts({ openMode, allowedHosts: config.MCP_ALLOWED_HOSTS, bindHost, advertisedHost });
|
|
112
|
+
if (allowedHosts) {
|
|
113
|
+
const rejectHost = (req, reason, value) => logger.warn('[HTTP] Rejected request: hostname not allowed (DNS rebinding protection, #612)', {
|
|
114
|
+
header: reason, hostname: value.slice(0, 253), path: req.path,
|
|
115
|
+
});
|
|
116
|
+
const sdkHostCheck = hostHeaderValidation(allowedHosts);
|
|
117
|
+
app.use((req, res, next) => {
|
|
118
|
+
let passed = false;
|
|
119
|
+
sdkHostCheck(req, res, () => { passed = true; });
|
|
120
|
+
if (passed) {
|
|
121
|
+
next();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// The SDK middleware is synchronous: it already answered 403.
|
|
125
|
+
let hostname = String(req.headers.host ?? '');
|
|
126
|
+
try {
|
|
127
|
+
hostname = new URL(`http://${hostname}`).hostname;
|
|
128
|
+
}
|
|
129
|
+
catch { /* log the raw value */ }
|
|
130
|
+
rejectHost(req, 'host', hostname);
|
|
131
|
+
});
|
|
132
|
+
if (openMode) {
|
|
133
|
+
// The SDK checks Host only. In open mode a browser Origin must also be an allowed
|
|
134
|
+
// hostname; an absent Origin (non-browser clients) passes. Comparing Origin to Host
|
|
135
|
+
// would not help: under rebinding both carry the attacker's name. /.well-known/* is
|
|
136
|
+
// exempt because mcp-auth serves it with cors() for public cross-origin reads.
|
|
137
|
+
app.use((req, res, next) => {
|
|
138
|
+
const origin = req.headers.origin;
|
|
139
|
+
if (origin === undefined || req.path.startsWith('/.well-known/')) {
|
|
140
|
+
next();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let hostname = null;
|
|
144
|
+
try {
|
|
145
|
+
hostname = origin === 'null' ? null : new URL(origin).hostname;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
hostname = null;
|
|
149
|
+
}
|
|
150
|
+
if (hostname !== null && allowedHosts.includes(hostname)) {
|
|
151
|
+
next();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
rejectHost(req, 'origin', hostname ?? 'null');
|
|
155
|
+
res.status(403).json({ jsonrpc: '2.0', error: { code: -32000, message: `Invalid Origin: ${hostname ?? 'null'}` }, id: null });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (openMode && config.MCP_ALLOWED_HOSTS.length === 0 && (bindHost === '0.0.0.0' || bindHost === '::')) {
|
|
159
|
+
logger.info(`[HTTP] Accepting Host ${allowedHosts.join(', ')} only (DNS rebinding protection, #612). Reach this server by another name? Add it to MCP_ALLOWED_HOSTS.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
89
162
|
// Explicit body-size cap (#168). An oversized payload is rejected with HTTP 413
|
|
90
163
|
// rather than buffered unbounded. Tunable via MCP_HTTP_BODY_LIMIT (default 512kb).
|
|
91
164
|
app.use(express.json({ limit: config.MCP_HTTP_BODY_LIMIT }));
|
|
@@ -366,7 +439,7 @@ advertisedUrl) {
|
|
|
366
439
|
// (#572). Only when NEITHER is configured is every request allowed. Static tokens are
|
|
367
440
|
// authentication, so MCP_ALLOW_UNAUTHENTICATED (a posture opt-out for the no-auth
|
|
368
441
|
// case) does not relax this.
|
|
369
|
-
if (
|
|
442
|
+
if (openMode) {
|
|
370
443
|
return true;
|
|
371
444
|
}
|
|
372
445
|
const authHeader = req.headers.authorization;
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.25.1",
|
|
5
|
+
"mcpName": "io.github.agigante80/actual-mcp-server",
|
|
5
6
|
"engines": {
|
|
6
7
|
"node": ">=22.0.0",
|
|
7
8
|
"npm": ">=10.0.0"
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"verify-tools": "npm run build && node scripts/verify-tools.js",
|
|
31
32
|
"check:coverage": "node scripts/list-actual-api-methods.mjs",
|
|
32
33
|
"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/install_snippets.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_acl_listing.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/multi_server_budget_selection.test.js && node tests/unit/static_tokens.test.js && node tests/unit/httpServer_static_tokens.test.js && node tests/unit/multi_server_budget_selection_stdio.test.js && node tests/unit/api_login_fetch.test.js && node tests/unit/multi_server_session_open_http.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/harness_cleanup.test.js && node tests/unit/harness_stdio_data_dir.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/rules_stage342_cleanup.test.js && node tests/unit/residue_unreadable.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js && node tests/unit/session_admission_race.test.js && node tests/unit/pool_admission_reservation.test.js && node tests/unit/config_max_sessions_per_principal.test.js && node tests/unit/session_principal_cap.test.js && node tests/unit/schema_published_parity.test.js && node tests/unit/manual_borrowed_category.test.js && node tests/unit/manual_budget_no_borrowed_writes.test.js && node tests/unit/refresh_test_budget_snapshot.test.js && node tests/unit/docs_check.test.js && node tests/unit/docs_markers.test.js && node tests/unit/docs_links_fixture.test.js && node tests/unit/docs_index.test.js && node tests/unit/docs_tools.test.js && node tests/unit/docs_readme_size.test.js && node tests/unit/template_lockstep.test.js && node tests/unit/client_examples.test.js",
|
|
34
|
+
"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/install_snippets.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_acl_listing.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/multi_server_budget_selection.test.js && node tests/unit/static_tokens.test.js && node tests/unit/httpServer_static_tokens.test.js && node tests/unit/httpServer_host_validation.test.js && node tests/unit/multi_server_budget_selection_stdio.test.js && node tests/unit/api_login_fetch.test.js && node tests/unit/multi_server_session_open_http.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/tool_content_logging.test.js && node tests/unit/adapter_error_logging.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/harness_cleanup.test.js && node tests/unit/harness_stdio_data_dir.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/rules_stage342_cleanup.test.js && node tests/unit/residue_unreadable.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/schedules_update_amount_guard.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js && node tests/unit/transactions_merge_adapter.test.js && node tests/unit/schedules_dead_conditions.test.js && node tests/unit/session_ownership_express.test.js && node tests/unit/pool_orphan_sweep.test.js && node tests/unit/session_budget_state_cleanup.test.js && node tests/unit/http_session_delete.test.js && node tests/unit/budget_acl_parse.test.js && node tests/unit/server_instructions.test.js && node tests/unit/query_run_empty_hint.test.js && node tests/unit/session_admission_race.test.js && node tests/unit/pool_admission_reservation.test.js && node tests/unit/config_max_sessions_per_principal.test.js && node tests/unit/session_principal_cap.test.js && node tests/unit/schema_published_parity.test.js && node tests/unit/manual_borrowed_category.test.js && node tests/unit/manual_budget_no_borrowed_writes.test.js && node tests/unit/refresh_test_budget_snapshot.test.js && node tests/unit/docs_check.test.js && node tests/unit/docs_markers.test.js && node tests/unit/docs_links_fixture.test.js && node tests/unit/docs_index.test.js && node tests/unit/docs_tools.test.js && node tests/unit/docs_readme_size.test.js && node tests/unit/template_lockstep.test.js && node tests/unit/client_examples.test.js && node tests/unit/mcp_server_json.test.js",
|
|
34
35
|
"test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
|
|
35
36
|
"test:e2e": "npx playwright test",
|
|
36
37
|
"test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
"@playwright/test": "^1.64.1",
|
|
90
91
|
"@types/express": "^5.0.3",
|
|
91
92
|
"@types/node": "^26.6.5",
|
|
92
|
-
"knip": "^6.
|
|
93
|
+
"knip": "^6.44.0",
|
|
93
94
|
"node-fetch": "^3.3.2",
|
|
94
95
|
"tsconfig-paths": "^4.2.0",
|
|
95
96
|
"typescript": "^7.0.2",
|