@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.
@@ -10,30 +10,145 @@
10
10
  * no express or other framework needed.
11
11
  *
12
12
  * Security:
13
- * - Binds to 127.0.0.1 only (localhost)
14
- * - DNS rebinding protection via Origin header check
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: reject cross-origin requests from non-local origins
45
- const origin = req.headers.origin;
46
- if (origin) {
47
- try {
48
- const url = new URL(origin);
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
- // Auth check: if MCP_AUTH_REQUIRED env var is set, validate JWT
62
- const authRequired = process.env.MCP_AUTH_REQUIRED === "true";
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
- // Allow unauthenticated health checks on root
66
- const checkPath = new URL(req.url || "/", `http://${host}:${port}`).pathname;
67
- if (checkPath !== "/") {
68
- claims = extractAuthClaims(req);
69
- if (!claims) {
70
- res.writeHead(401, { "Content-Type": "application/json" });
71
- res.end(JSON.stringify({ error: "Authentication required" }));
72
- return;
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.writeHead(200, { "Content-Type": "application/json" });
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.writeHead(404, { "Content-Type": "application/json" });
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
- await handleMcpRequest(req, res);
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
- httpServer.listen(port, host, () => {
100
- console.error(`4DA MCP Server v${SERVER_VERSION} (Streamable HTTP) listening on http://${host}:${port}/mcp`);
101
- console.error(`Health check: http://${host}:${port}/`);
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 mcpHandler.close();
107
- httpServer.close();
108
- process.exit(0);
217
+ void close().finally(() => process.exit(0));
109
218
  };
110
- process.on("SIGINT", shutdown);
111
- process.on("SIGTERM", shutdown);
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: the server trusts its local environment and does NOT implement
12
- * authentication. The optional --http transport is intended for localhost use;
13
- * do NOT expose it over a network without putting your own auth in front.
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 portIndex = args.indexOf("--port");
369
- const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 4840;
370
- const host = args.includes("--host") ? args[args.indexOf("--host") + 1] : "127.0.0.1";
371
- await startHttpServer(buildServer, { port, host });
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
@@ -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 */
@@ -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.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
- "pnpm": {
98
- "onlyBuiltDependencies": [
99
- "better-sqlite3",
100
- "esbuild"
101
- ],
102
- "overrides": {
103
- "fast-uri": ">=3.1.2",
104
- "ip-address": "^10.1.1",
105
- "vite": ">=7.3.5 <8",
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