@4da/mcp-server 5.0.0 → 5.0.2
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/auth-context.d.ts +49 -0
- package/dist/auth-context.js +72 -0
- package/dist/auth.d.ts +42 -15
- package/dist/auth.js +160 -49
- package/dist/http-transport.d.ts +61 -3
- package/dist/http-transport.js +159 -49
- package/dist/index.js +46 -8
- package/dist/setup.js +0 -0
- package/dist/tool-dispatch.d.ts +4 -0
- package/dist/tool-dispatch.js +6 -0
- package/package.json +13 -25
- package/dist/llm.d.ts +0 -162
- package/dist/llm.js +0 -502
- package/dist/output-manager.d.ts +0 -156
- package/dist/output-manager.js +0 -157
- package/dist/scheduler/core.d.ts +0 -25
- package/dist/scheduler/core.js +0 -61
- package/dist/scheduler/freshness.d.ts +0 -17
- package/dist/scheduler/freshness.js +0 -41
- package/dist/scheduler/index.d.ts +0 -21
- package/dist/scheduler/index.js +0 -32
- package/dist/scheduler/refreshers.d.ts +0 -21
- package/dist/scheduler/refreshers.js +0 -21
package/dist/http-transport.js
CHANGED
|
@@ -10,30 +10,145 @@
|
|
|
10
10
|
* no express or other framework needed.
|
|
11
11
|
*
|
|
12
12
|
* Security:
|
|
13
|
-
* - Binds to 127.0.0.1
|
|
14
|
-
*
|
|
13
|
+
* - Binds to 127.0.0.1 by default. `--host` can widen that; a non-loopback
|
|
14
|
+
* bind is refused unless a shared auth secret is configured, and forces
|
|
15
|
+
* authentication on for every request.
|
|
16
|
+
* - DNS rebinding protection via a `Host` header allowlist, applied to EVERY
|
|
17
|
+
* request. This is the primary guard: a rebinding attack necessarily
|
|
18
|
+
* presents the attacker's hostname in `Host`, and unlike `Origin` the header
|
|
19
|
+
* is mandatory, so a request cannot skip the check by omitting it.
|
|
20
|
+
* - `Origin`, when present, must also be localhost-class. A missing `Origin`
|
|
21
|
+
* is not a pass — it is simply not additional evidence, so such requests are
|
|
22
|
+
* admitted only on the strength of the `Host` check above, and only ever
|
|
23
|
+
* without credentials on a loopback bind.
|
|
24
|
+
* - Bearer tokens are HMAC-SHA256 verified (see auth.ts) before any claim is
|
|
25
|
+
* read, and the verified role is enforced on tool dispatch.
|
|
15
26
|
* - Stateless (no session tracking)
|
|
16
27
|
*/
|
|
17
28
|
import { createServer } from "node:http";
|
|
18
29
|
import { readFileSync } from "node:fs";
|
|
19
30
|
import { join, dirname } from "node:path";
|
|
20
31
|
import { fileURLToPath } from "node:url";
|
|
21
|
-
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
32
|
+
import { createMcpHandler, localhostAllowedHostnames, localhostAllowedOrigins, validateHostHeader, validateOriginHeader, } from "@modelcontextprotocol/server";
|
|
22
33
|
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
23
|
-
import { extractAuthClaims } from "./auth.js";
|
|
34
|
+
import { extractAuthClaims, getAuthSecret } from "./auth.js";
|
|
35
|
+
import { runWithAuthContext } from "./auth-context.js";
|
|
24
36
|
const DEFAULT_PORT = 4840;
|
|
25
37
|
const DEFAULT_HOST = "127.0.0.1";
|
|
26
38
|
const __ht_dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
39
|
const SERVER_VERSION = JSON.parse(readFileSync(join(__ht_dirname, "..", "package.json"), "utf-8")).version;
|
|
40
|
+
/** Strip IPv6 brackets and zone id so "[::1]" and "::1" compare equal. */
|
|
41
|
+
function normalizeHostname(host) {
|
|
42
|
+
return host.trim().replace(/^\[|\]$/g, "").split("%")[0].toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
/** Loopback bind addresses — everything else reaches the network. */
|
|
45
|
+
export function isLoopbackBind(host) {
|
|
46
|
+
const h = normalizeHostname(host);
|
|
47
|
+
return h === "localhost" || h === "::1" || /^127\./.test(h);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extra hostnames accepted in `Host`/`Origin`, from `MCP_ALLOWED_HOSTS`
|
|
51
|
+
* (comma-separated). Needed when binding to `0.0.0.0`, where the hostname
|
|
52
|
+
* legitimate clients use is not knowable from the bind address.
|
|
53
|
+
*/
|
|
54
|
+
function configuredExtraHosts() {
|
|
55
|
+
return (process.env.MCP_ALLOWED_HOSTS ?? "")
|
|
56
|
+
.split(",")
|
|
57
|
+
.map((h) => normalizeHostname(h))
|
|
58
|
+
.filter((h) => h.length > 0);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the security policy for a bind address.
|
|
62
|
+
*
|
|
63
|
+
* A loopback bind keeps the historical behaviour: auth is opt-in via
|
|
64
|
+
* `MCP_AUTH_REQUIRED`. Any other bind is network-exposed, so authentication is
|
|
65
|
+
* mandatory regardless of that variable.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveSecurityPolicy(host) {
|
|
68
|
+
const loopback = isLoopbackBind(host);
|
|
69
|
+
const extras = configuredExtraHosts();
|
|
70
|
+
// A concrete non-loopback bind address is itself a legitimate Host value;
|
|
71
|
+
// wildcards (0.0.0.0 / ::) name no reachable host, so they add nothing.
|
|
72
|
+
const bindHostname = normalizeHostname(host);
|
|
73
|
+
const isWildcard = bindHostname === "0.0.0.0" || bindHostname === "::" || bindHostname === "";
|
|
74
|
+
const bindExtras = loopback || isWildcard ? [] : [bindHostname];
|
|
75
|
+
return {
|
|
76
|
+
loopback,
|
|
77
|
+
authRequired: loopback ? process.env.MCP_AUTH_REQUIRED === "true" : true,
|
|
78
|
+
allowedHostnames: [...localhostAllowedHostnames(), ...bindExtras, ...extras],
|
|
79
|
+
allowedOrigins: [...localhostAllowedOrigins(), ...bindExtras, ...extras],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* DNS rebinding guard. Runs on every request, before routing and before auth.
|
|
84
|
+
*
|
|
85
|
+
* Returns null when the request may proceed, or the rejection to send.
|
|
86
|
+
*/
|
|
87
|
+
export function evaluateRequestGuard(headers, policy) {
|
|
88
|
+
const pick = (v) => Array.isArray(v) ? v[0] : v;
|
|
89
|
+
// `Host` is mandatory in HTTP/1.1 and always present in HTTP/2 (as
|
|
90
|
+
// :authority, which Node surfaces here). Missing means malformed, not
|
|
91
|
+
// trusted — validateHostHeader rejects it.
|
|
92
|
+
const hostResult = validateHostHeader(pick(headers.host), policy.allowedHostnames);
|
|
93
|
+
if (!hostResult.ok) {
|
|
94
|
+
return { status: 403, error: `Forbidden: DNS rebinding protection (${hostResult.errorCode})` };
|
|
95
|
+
}
|
|
96
|
+
// `Origin` is only sent by browsers. A present-but-foreign value is a
|
|
97
|
+
// cross-origin browser request and is refused; an absent value adds no
|
|
98
|
+
// evidence either way, and the Host check above has already run.
|
|
99
|
+
const originResult = validateOriginHeader(pick(headers.origin), policy.allowedOrigins);
|
|
100
|
+
if (!originResult.ok) {
|
|
101
|
+
return { status: 403, error: `Forbidden: ${originResult.errorCode}` };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function sendJson(res, status, body) {
|
|
106
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
107
|
+
res.end(JSON.stringify(body));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Refuse to start a network-exposed server that cannot authenticate anyone,
|
|
111
|
+
* and make a non-loopback bind impossible to do accidentally.
|
|
112
|
+
*/
|
|
113
|
+
export function enforceBindPolicy(host, policy) {
|
|
114
|
+
if (policy.loopback)
|
|
115
|
+
return;
|
|
116
|
+
if (!getAuthSecret()) {
|
|
117
|
+
throw new Error(`Refusing to bind the MCP HTTP transport to a non-loopback address (${host}) with no auth secret.\n` +
|
|
118
|
+
` Every 4DA tool (dependency inventory, decisions, agent memory, Developer DNA) would be\n` +
|
|
119
|
+
` readable and writable by anyone who can reach this port.\n` +
|
|
120
|
+
` Set MCP_AUTH_SECRET (the same value as the relay's JWT_SECRET) to enable token verification,\n` +
|
|
121
|
+
` or drop --host and serve on 127.0.0.1.`);
|
|
122
|
+
}
|
|
123
|
+
console.error("");
|
|
124
|
+
console.error(" ############################################################");
|
|
125
|
+
console.error(` # WARNING: MCP HTTP transport is bound to ${host}`);
|
|
126
|
+
console.error(" # This exposes 4DA tools beyond this machine.");
|
|
127
|
+
console.error(" # Authentication is REQUIRED for every request on this bind.");
|
|
128
|
+
console.error(" # Put it behind TLS and a trusted network boundary.");
|
|
129
|
+
console.error(" ############################################################");
|
|
130
|
+
console.error("");
|
|
131
|
+
const extras = configuredExtraHosts();
|
|
132
|
+
const bindHostname = normalizeHostname(host);
|
|
133
|
+
if ((bindHostname === "0.0.0.0" || bindHostname === "::") && extras.length === 0) {
|
|
134
|
+
console.error(" NOTE: MCP_ALLOWED_HOSTS is unset, so only localhost-class Host headers are accepted.");
|
|
135
|
+
console.error(" Set MCP_ALLOWED_HOSTS=<hostname-or-ip[,...]> to the address clients actually use.");
|
|
136
|
+
console.error("");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
28
139
|
/**
|
|
29
140
|
* Start a Streamable HTTP server for the MCP protocol.
|
|
30
141
|
*
|
|
31
142
|
* Takes a server factory: `createMcpHandler` builds a fresh instance per
|
|
32
143
|
* request (stateless — compatible with serverless environments and simple
|
|
33
144
|
* round-robin load balancing).
|
|
145
|
+
*
|
|
146
|
+
* Resolves once the server is accepting connections.
|
|
34
147
|
*/
|
|
35
148
|
export async function startHttpServer(factory, options = { port: DEFAULT_PORT, host: DEFAULT_HOST }) {
|
|
36
149
|
const { port, host } = options;
|
|
150
|
+
const policy = resolveSecurityPolicy(host);
|
|
151
|
+
enforceBindPolicy(host, policy);
|
|
37
152
|
const mcpHandler = createMcpHandler(factory);
|
|
38
153
|
const handleMcpRequest = toNodeHandler(mcpHandler, {
|
|
39
154
|
onerror: (err) => {
|
|
@@ -41,73 +156,68 @@ export async function startHttpServer(factory, options = { port: DEFAULT_PORT, h
|
|
|
41
156
|
},
|
|
42
157
|
});
|
|
43
158
|
const httpServer = createServer(async (req, res) => {
|
|
44
|
-
// DNS rebinding protection
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "::1" && url.hostname !== "[::1]") {
|
|
50
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
51
|
-
res.end(JSON.stringify({ error: "Forbidden: DNS rebinding protection" }));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
57
|
-
res.end(JSON.stringify({ error: "Forbidden: invalid Origin header" }));
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
159
|
+
// DNS rebinding protection — every request, before routing or auth.
|
|
160
|
+
const rejection = evaluateRequestGuard(req.headers, policy);
|
|
161
|
+
if (rejection) {
|
|
162
|
+
sendJson(res, rejection.status, { error: rejection.error });
|
|
163
|
+
return;
|
|
60
164
|
}
|
|
61
|
-
|
|
62
|
-
|
|
165
|
+
const pathname = new URL(req.url || "/", `http://${host}:${port}`).pathname;
|
|
166
|
+
// Auth gate. Unauthenticated health checks on "/" stay open so liveness
|
|
167
|
+
// probes work; the endpoint reveals only name, version and status.
|
|
63
168
|
let claims = null;
|
|
64
|
-
if (authRequired) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
169
|
+
if (policy.authRequired && pathname !== "/") {
|
|
170
|
+
claims = extractAuthClaims(req);
|
|
171
|
+
if (!claims) {
|
|
172
|
+
res.writeHead(401, {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
"WWW-Authenticate": 'Bearer realm="4da-mcp"',
|
|
175
|
+
});
|
|
176
|
+
res.end(JSON.stringify({ error: "Authentication required" }));
|
|
177
|
+
return;
|
|
74
178
|
}
|
|
75
179
|
}
|
|
76
|
-
// Route requests
|
|
77
|
-
const pathname = new URL(req.url || "/", `http://${host}:${port}`).pathname;
|
|
78
180
|
// Health check endpoint
|
|
79
181
|
if (pathname === "/" && req.method === "GET") {
|
|
80
|
-
res
|
|
81
|
-
res.end(JSON.stringify({
|
|
182
|
+
sendJson(res, 200, {
|
|
82
183
|
name: "4da-mcp",
|
|
83
184
|
version: SERVER_VERSION,
|
|
84
185
|
transport: "streamable-http",
|
|
85
186
|
status: "ok",
|
|
86
|
-
})
|
|
187
|
+
});
|
|
87
188
|
return;
|
|
88
189
|
}
|
|
89
190
|
// Only handle /mcp endpoint
|
|
90
191
|
if (pathname !== "/mcp") {
|
|
91
|
-
res
|
|
92
|
-
res.end(JSON.stringify({ error: "Not Found" }));
|
|
192
|
+
sendJson(res, 404, { error: "Not Found" });
|
|
93
193
|
return;
|
|
94
194
|
}
|
|
95
195
|
// Hand off to the MCP handler — body parsing, era classification, and
|
|
96
|
-
// per-request instance construction happen inside the entry.
|
|
97
|
-
|
|
196
|
+
// per-request instance construction happen inside the entry. The auth
|
|
197
|
+
// context rides along so tool dispatch can enforce the verified role.
|
|
198
|
+
await runWithAuthContext({ claims, enforced: policy.authRequired }, () => handleMcpRequest(req, res));
|
|
98
199
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
200
|
+
await new Promise((resolve, reject) => {
|
|
201
|
+
httpServer.once("error", reject);
|
|
202
|
+
httpServer.listen(port, host, () => {
|
|
203
|
+
httpServer.removeListener("error", reject);
|
|
204
|
+
resolve();
|
|
205
|
+
});
|
|
102
206
|
});
|
|
207
|
+
console.error(`4DA MCP Server v${SERVER_VERSION} (Streamable HTTP) listening on http://${host}:${port}/mcp`);
|
|
208
|
+
console.error(`Health check: http://${host}:${port}/`);
|
|
209
|
+
console.error(`Authentication: ${policy.authRequired ? "required (HMAC-SHA256 verified Bearer tokens)" : "disabled (loopback only)"}`);
|
|
210
|
+
const close = async () => {
|
|
211
|
+
await mcpHandler.close();
|
|
212
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
213
|
+
};
|
|
103
214
|
// Graceful shutdown
|
|
104
215
|
const shutdown = () => {
|
|
105
216
|
console.error("Shutting down HTTP server...");
|
|
106
|
-
void
|
|
107
|
-
httpServer.close();
|
|
108
|
-
process.exit(0);
|
|
217
|
+
void close().finally(() => process.exit(0));
|
|
109
218
|
};
|
|
110
|
-
process.
|
|
111
|
-
process.
|
|
219
|
+
process.once("SIGINT", shutdown);
|
|
220
|
+
process.once("SIGTERM", shutdown);
|
|
221
|
+
return { server: httpServer, close };
|
|
112
222
|
}
|
|
113
223
|
//# sourceMappingURL=http-transport.js.map
|
package/dist/index.js
CHANGED
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* Claude Desktop, and other MCP hosts. Runs locally; the only data leaving the
|
|
9
9
|
* machine is public package names/versions sent to registries (OSV, npm, etc.).
|
|
10
10
|
*
|
|
11
|
-
* SECURITY:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* SECURITY: stdio serving is unauthenticated by design — the MCP host launches
|
|
12
|
+
* this process and already holds local process rights. The optional --http
|
|
13
|
+
* transport defaults to 127.0.0.1 and applies a Host-header DNS rebinding
|
|
14
|
+
* guard to every request. Binding it anywhere else requires a shared auth
|
|
15
|
+
* secret (MCP_AUTH_SECRET) and then verifies an HMAC-SHA256-signed Bearer
|
|
16
|
+
* token on every request, enforcing the token's role at tool dispatch.
|
|
14
17
|
*
|
|
15
18
|
* 14 tools across 5 categories. Live vulnerability scanning (OSV.dev),
|
|
16
19
|
* ecosystem news, persistent memory, and tech stack awareness for any MCP host.
|
|
@@ -338,13 +341,23 @@ async function main() {
|
|
|
338
341
|
Options:
|
|
339
342
|
--http Use Streamable HTTP instead of stdio
|
|
340
343
|
--port <number> HTTP port (default: 4840)
|
|
341
|
-
--host <address> HTTP bind address (default: 127.0.0.1)
|
|
344
|
+
--host <address> HTTP bind address (default: 127.0.0.1). A non-loopback
|
|
345
|
+
address exposes every tool to the network and is
|
|
346
|
+
refused unless MCP_AUTH_SECRET is set; authentication
|
|
347
|
+
is then mandatory for every request.
|
|
342
348
|
--setup Detect editors and write MCP config
|
|
343
349
|
--doctor Validate database, bindings, and LLM providers
|
|
344
350
|
|
|
345
351
|
Environment:
|
|
346
352
|
FOURDA_DB_PATH Path to 4DA's SQLite database (auto-detected if omitted)
|
|
347
353
|
FOURDA_OFFLINE Set to "true" to disable all network calls (OSV.dev, HN)
|
|
354
|
+
MCP_AUTH_SECRET Shared secret for verifying relay-issued Bearer tokens
|
|
355
|
+
(HMAC-SHA256). Falls back to JWT_SECRET. Without it, no
|
|
356
|
+
token is accepted.
|
|
357
|
+
MCP_AUTH_REQUIRED Set to "true" to require auth on a loopback --http bind
|
|
358
|
+
(always required on a non-loopback bind)
|
|
359
|
+
MCP_ALLOWED_HOSTS Extra comma-separated hostnames accepted in the Host /
|
|
360
|
+
Origin headers (needed when binding to 0.0.0.0)
|
|
348
361
|
|
|
349
362
|
Works standalone (scans your project on startup) or with the full
|
|
350
363
|
4DA desktop app for content scoring, source monitoring, and more.
|
|
@@ -365,10 +378,35 @@ async function main() {
|
|
|
365
378
|
}
|
|
366
379
|
// HTTP transport mode
|
|
367
380
|
if (args.includes("--http")) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
381
|
+
const flagValue = (flag) => {
|
|
382
|
+
const i = args.indexOf(flag);
|
|
383
|
+
if (i === -1)
|
|
384
|
+
return undefined;
|
|
385
|
+
const value = args[i + 1];
|
|
386
|
+
// A flag with no value (or followed by another flag) is a typo, not a
|
|
387
|
+
// request to bind everywhere — refuse rather than guess.
|
|
388
|
+
if (value === undefined || value.startsWith("-")) {
|
|
389
|
+
console.error(`[4DA] ${flag} requires a value`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
};
|
|
394
|
+
const rawPort = flagValue("--port");
|
|
395
|
+
const port = rawPort === undefined ? 4840 : Number.parseInt(rawPort, 10);
|
|
396
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
397
|
+
console.error(`[4DA] Invalid --port value: ${rawPort}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const host = flagValue("--host") ?? "127.0.0.1";
|
|
401
|
+
try {
|
|
402
|
+
await startHttpServer(buildServer, { port, host });
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
// Bind-policy refusals carry an actionable message; print it plainly
|
|
406
|
+
// rather than as an unhandled stack trace.
|
|
407
|
+
console.error(`[4DA] ${error instanceof Error ? error.message : String(error)}`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
372
410
|
return;
|
|
373
411
|
}
|
|
374
412
|
// -------------------------------------------------------------------------
|
package/dist/setup.js
CHANGED
|
File without changes
|
package/dist/tool-dispatch.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export type ToolExecutor = (db: FourDADatabase, params: any) => unknown | Promis
|
|
|
14
14
|
/**
|
|
15
15
|
* Dispatch a tool call by name. Awaits uniformly (no-op for sync executors).
|
|
16
16
|
* Returns MCP-formatted response with JSON-serialized result.
|
|
17
|
+
*
|
|
18
|
+
* Authenticated HTTP callers are role-checked here — the single choke point
|
|
19
|
+
* every tool call passes through. Stdio callers are unaffected (no auth
|
|
20
|
+
* context; the host already owns the process).
|
|
17
21
|
*/
|
|
18
22
|
export declare function dispatchTool(name: string, db: FourDADatabase, args: Record<string, unknown> | undefined): Promise<CallToolResult>;
|
|
19
23
|
/** Check if a tool exists in the dispatch map */
|
package/dist/tool-dispatch.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Map-based dispatch for the 14 active tools.
|
|
6
6
|
* Adding a new tool = add to this map + schema-registry + barrel export.
|
|
7
7
|
*/
|
|
8
|
+
import { assertToolPermission } from "./auth-context.js";
|
|
8
9
|
import { executeGetRelevantContent, executeGetContext, executeRecordFeedback, executeGetActionableSignals, executeKnowledgeGaps, executeDecisionMemory, executeCheckDecisionAlignment, executeAgentMemory, executeDeveloperDna, executeWhatShouldIKnow, executeVulnerabilityScan, executeEcosystemPulse, executeDependencyHealth, executeUpgradePlanner, } from "./tools/index.js";
|
|
9
10
|
import { getLiveIntelligence } from "./live-singleton.js";
|
|
10
11
|
/**
|
|
@@ -46,12 +47,17 @@ const FRESHNESS_TOOLS = new Set([
|
|
|
46
47
|
/**
|
|
47
48
|
* Dispatch a tool call by name. Awaits uniformly (no-op for sync executors).
|
|
48
49
|
* Returns MCP-formatted response with JSON-serialized result.
|
|
50
|
+
*
|
|
51
|
+
* Authenticated HTTP callers are role-checked here — the single choke point
|
|
52
|
+
* every tool call passes through. Stdio callers are unaffected (no auth
|
|
53
|
+
* context; the host already owns the process).
|
|
49
54
|
*/
|
|
50
55
|
export async function dispatchTool(name, db, args) {
|
|
51
56
|
const executor = DISPATCH_MAP[name];
|
|
52
57
|
if (!executor) {
|
|
53
58
|
throw new Error(`Unknown tool: ${name}`);
|
|
54
59
|
}
|
|
60
|
+
assertToolPermission(name);
|
|
55
61
|
const result = await executor(db, (args || {}));
|
|
56
62
|
const payload = FRESHNESS_TOOLS.has(name) ? attachFreshness(db, result) : result;
|
|
57
63
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4da/mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"mcpName": "io.github.4DA-Systems/4da-mcp-server",
|
|
5
5
|
"description": "Dependency intelligence for AI coding agents. Live CVE scanning, dependency health, upgrade planning, ecosystem news, decision memory — zero config, privacy-first. 9 tools standalone, 14 total with desktop app.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,17 +18,6 @@
|
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsc && pnpm run copy-schemas",
|
|
23
|
-
"copy-schemas": "node -e \"const fs=require('fs');const path=require('path');const src='src/schemas';const dst='dist/schemas';if(!fs.existsSync(dst))fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dst,f)))\"",
|
|
24
|
-
"dev": "tsc --watch",
|
|
25
|
-
"start": "node dist/index.js",
|
|
26
|
-
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
27
|
-
"setup": "node dist/setup.js",
|
|
28
|
-
"test": "vitest run",
|
|
29
|
-
"test:watch": "vitest",
|
|
30
|
-
"prepublishOnly": "pnpm run build"
|
|
31
|
-
},
|
|
32
21
|
"keywords": [
|
|
33
22
|
"mcp",
|
|
34
23
|
"mcp-server",
|
|
@@ -82,7 +71,8 @@
|
|
|
82
71
|
"dependencies": {
|
|
83
72
|
"better-sqlite3": "^12.9.0",
|
|
84
73
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
85
|
-
"@modelcontextprotocol/server": "^2.0.0"
|
|
74
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
75
|
+
"hono": "^4.12.34"
|
|
86
76
|
},
|
|
87
77
|
"devDependencies": {
|
|
88
78
|
"@types/better-sqlite3": "^7.6.11",
|
|
@@ -94,16 +84,14 @@
|
|
|
94
84
|
"engines": {
|
|
95
85
|
"node": ">=20.0.0"
|
|
96
86
|
},
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"esbuild": ">=0.28.1"
|
|
107
|
-
}
|
|
87
|
+
"scripts": {
|
|
88
|
+
"build": "tsc && pnpm run copy-schemas",
|
|
89
|
+
"copy-schemas": "node -e \"const fs=require('fs');const path=require('path');const src='src/schemas';const dst='dist/schemas';if(!fs.existsSync(dst))fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dst,f)))\"",
|
|
90
|
+
"dev": "tsc --watch",
|
|
91
|
+
"start": "node dist/index.js",
|
|
92
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
93
|
+
"setup": "node dist/setup.js",
|
|
94
|
+
"test": "vitest run",
|
|
95
|
+
"test:watch": "vitest"
|
|
108
96
|
}
|
|
109
|
-
}
|
|
97
|
+
}
|
package/dist/llm.d.ts
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM Client for 4DA MCP Server
|
|
3
|
-
*
|
|
4
|
-
* Provides intelligence synthesis over raw tool data.
|
|
5
|
-
* Supports Anthropic Claude, OpenAI, and Ollama (local).
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Tiered model selection (use smaller models for simpler tasks)
|
|
9
|
-
* - Optimized prompts for local models
|
|
10
|
-
* - Automatic fallback
|
|
11
|
-
*/
|
|
12
|
-
export interface LLMConfig {
|
|
13
|
-
provider: "anthropic" | "openai" | "ollama" | null;
|
|
14
|
-
anthropic_api_key?: string;
|
|
15
|
-
openai_api_key?: string;
|
|
16
|
-
ollama_url?: string;
|
|
17
|
-
model?: string;
|
|
18
|
-
model_light?: string;
|
|
19
|
-
model_heavy?: string;
|
|
20
|
-
}
|
|
21
|
-
export type TaskComplexity = "light" | "medium" | "heavy";
|
|
22
|
-
export interface SynthesisRequest {
|
|
23
|
-
system: string;
|
|
24
|
-
prompt: string;
|
|
25
|
-
max_tokens?: number;
|
|
26
|
-
complexity?: TaskComplexity;
|
|
27
|
-
}
|
|
28
|
-
export interface SynthesisResult {
|
|
29
|
-
synthesis: string;
|
|
30
|
-
model_used: string;
|
|
31
|
-
tokens_used?: number;
|
|
32
|
-
latency_ms?: number;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Default models for different task complexities
|
|
36
|
-
* Optimized for 16GB VRAM
|
|
37
|
-
*/
|
|
38
|
-
export declare const DEFAULT_OLLAMA_MODELS: {
|
|
39
|
-
light: string;
|
|
40
|
-
medium: string;
|
|
41
|
-
heavy: string;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Task complexity by tool
|
|
45
|
-
*/
|
|
46
|
-
export declare const TOOL_COMPLEXITY: Record<string, TaskComplexity>;
|
|
47
|
-
/**
|
|
48
|
-
* Get LLM configuration from settings.json file.
|
|
49
|
-
*
|
|
50
|
-
* Uses a cached approach: the file is only re-read if its mtime has changed,
|
|
51
|
-
* avoiding unnecessary filesystem reads on every tool invocation.
|
|
52
|
-
*/
|
|
53
|
-
export declare function getLLMConfig(_db?: {
|
|
54
|
-
prepare: (sql: string) => {
|
|
55
|
-
get: (...args: unknown[]) => unknown;
|
|
56
|
-
};
|
|
57
|
-
}): LLMConfig;
|
|
58
|
-
/**
|
|
59
|
-
* Check if LLM synthesis is available
|
|
60
|
-
*/
|
|
61
|
-
export declare function canSynthesize(config: LLMConfig): boolean;
|
|
62
|
-
/**
|
|
63
|
-
* Synthesize insights using LLM
|
|
64
|
-
*
|
|
65
|
-
* Accepts an optional AbortSignal to allow timeout/cancellation of the
|
|
66
|
-
* underlying fetch calls.
|
|
67
|
-
*/
|
|
68
|
-
export declare function synthesize(config: LLMConfig, request: SynthesisRequest, signal?: AbortSignal): Promise<SynthesisResult>;
|
|
69
|
-
/**
|
|
70
|
-
* Synthesize with a timeout wrapper.
|
|
71
|
-
*
|
|
72
|
-
* Wraps the LLM API call with an AbortController + configurable timeout.
|
|
73
|
-
* On timeout, throws a descriptive error so callers can fall back to
|
|
74
|
-
* data-only results.
|
|
75
|
-
*
|
|
76
|
-
* @param config - LLM provider configuration
|
|
77
|
-
* @param request - Synthesis request (system prompt, user prompt, etc.)
|
|
78
|
-
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
79
|
-
*/
|
|
80
|
-
export declare function synthesizeWithTimeout(config: LLMConfig, request: SynthesisRequest, timeoutMs?: number): Promise<SynthesisResult>;
|
|
81
|
-
/**
|
|
82
|
-
* Prompts are optimized for:
|
|
83
|
-
* 1. Clear structure (local models need explicit formatting)
|
|
84
|
-
* 2. Concise output (fewer tokens = faster inference)
|
|
85
|
-
* 3. Specific instructions (reduce ambiguity)
|
|
86
|
-
*/
|
|
87
|
-
export declare const SYNTHESIS_PROMPTS: {
|
|
88
|
-
/**
|
|
89
|
-
* Daily Briefing - Executive synthesis
|
|
90
|
-
* Complexity: MEDIUM
|
|
91
|
-
*/
|
|
92
|
-
dailyBriefing: {
|
|
93
|
-
complexity: TaskComplexity;
|
|
94
|
-
system: string;
|
|
95
|
-
buildPrompt: (data: unknown, context: unknown) => string;
|
|
96
|
-
};
|
|
97
|
-
/**
|
|
98
|
-
* Score Autopsy - Explain the reasoning
|
|
99
|
-
* Complexity: HEAVY (needs to reason about correctness)
|
|
100
|
-
*/
|
|
101
|
-
scoreAutopsy: {
|
|
102
|
-
complexity: TaskComplexity;
|
|
103
|
-
system: string;
|
|
104
|
-
buildPrompt: (data: unknown, context: unknown) => string;
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* Trend Analysis - Predict and explain
|
|
108
|
-
* Complexity: HEAVY (needs pattern recognition + prediction)
|
|
109
|
-
*/
|
|
110
|
-
trendAnalysis: {
|
|
111
|
-
complexity: TaskComplexity;
|
|
112
|
-
system: string;
|
|
113
|
-
buildPrompt: (data: unknown, context: unknown) => string;
|
|
114
|
-
};
|
|
115
|
-
/**
|
|
116
|
-
* Context Analysis - Personalized optimization advice
|
|
117
|
-
* Complexity: MEDIUM (structured recommendations)
|
|
118
|
-
*/
|
|
119
|
-
contextAnalysis: {
|
|
120
|
-
complexity: TaskComplexity;
|
|
121
|
-
system: string;
|
|
122
|
-
buildPrompt: (data: unknown) => string;
|
|
123
|
-
};
|
|
124
|
-
/**
|
|
125
|
-
* Topic Connections - Explain the knowledge graph
|
|
126
|
-
* Complexity: HEAVY (needs graph reasoning)
|
|
127
|
-
*/
|
|
128
|
-
topicConnections: {
|
|
129
|
-
complexity: TaskComplexity;
|
|
130
|
-
system: string;
|
|
131
|
-
buildPrompt: (data: unknown, context: unknown) => string;
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
/**
|
|
135
|
-
* Check if Ollama is running and has required models
|
|
136
|
-
*/
|
|
137
|
-
export declare function checkOllamaStatus(baseUrl?: string): Promise<{
|
|
138
|
-
available: boolean;
|
|
139
|
-
models: string[];
|
|
140
|
-
error?: string;
|
|
141
|
-
}>;
|
|
142
|
-
/**
|
|
143
|
-
* Recommended models for 4DA synthesis tasks
|
|
144
|
-
*/
|
|
145
|
-
export declare const RECOMMENDED_MODELS: {
|
|
146
|
-
"16GB_VRAM": {
|
|
147
|
-
primary: string;
|
|
148
|
-
fallback: string;
|
|
149
|
-
install: string;
|
|
150
|
-
};
|
|
151
|
-
"12GB_VRAM": {
|
|
152
|
-
primary: string;
|
|
153
|
-
fallback: string;
|
|
154
|
-
install: string;
|
|
155
|
-
};
|
|
156
|
-
"8GB_VRAM": {
|
|
157
|
-
primary: string;
|
|
158
|
-
fallback: string;
|
|
159
|
-
install: string;
|
|
160
|
-
};
|
|
161
|
-
};
|
|
162
|
-
//# sourceMappingURL=llm.d.ts.map
|