prism-mcp-server 19.2.6 → 19.2.8

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.
@@ -558,7 +558,7 @@ return false;}
558
558
  // GET /api/skills → { skills: { dev: "...", qa: "..." } }
559
559
  if (url.pathname === "/api/skills" && req.method === "GET") {
560
560
  const all = await getAllSettings();
561
- const skills = {};
561
+ const skills = Object.create(null);
562
562
  for (const [k, v] of Object.entries(all)) {
563
563
  if (k.startsWith("skill:") && v) {
564
564
  skills[k.replace("skill:", "")] = v;
@@ -3,7 +3,7 @@ import { getStorage } from "../storage/index.js";
3
3
  import { debugLog } from "../utils/logger.js";
4
4
  import { getLLMProvider } from "../utils/llm/factory.js";
5
5
  import { randomUUID } from "node:crypto";
6
- import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { dirname, join } from "node:path";
8
8
  import { homedir } from "node:os";
9
9
  import { performWebSearchRaw } from "../utils/braveApi.js";
@@ -115,20 +115,53 @@ function acquireFileLock() {
115
115
  const lockDir = dirname(LOCK_PATH);
116
116
  if (!existsSync(lockDir))
117
117
  mkdirSync(lockDir, { recursive: true });
118
- if (existsSync(LOCK_PATH)) {
119
- const stat = statSync(LOCK_PATH);
120
- const ageMs = Date.now() - stat.mtimeMs;
121
- if (ageMs < LOCK_STALE_MS)
118
+ const lockContent = `${process.pid}\n${new Date().toISOString()}`;
119
+ // Fast path: exclusive create — no lock exists.
120
+ try {
121
+ writeFileSync(LOCK_PATH, lockContent, { flag: "wx" });
122
+ return true;
123
+ }
124
+ catch (wxErr) {
125
+ if (wxErr?.code !== "EEXIST")
122
126
  return false;
123
- unlinkSync(LOCK_PATH);
124
127
  }
125
- // wx = exclusive create throws EEXIST if another process won the race
126
- writeFileSync(LOCK_PATH, `${process.pid}\n${new Date().toISOString()}`, { flag: "wx" });
128
+ // Lock existscheck if stale.
129
+ let stat;
130
+ try {
131
+ stat = statSync(LOCK_PATH);
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ if (Date.now() - stat.mtimeMs < LOCK_STALE_MS)
137
+ return false;
138
+ // Stale lock — atomic takeover via rename.
139
+ // Write our identity to a temp file, then rename over the lock.
140
+ // rename() is atomic on POSIX; no window where the lock is absent.
141
+ const tmpLock = `${LOCK_PATH}.${process.pid}`;
142
+ try {
143
+ writeFileSync(tmpLock, lockContent, { flag: "wx" });
144
+ renameSync(tmpLock, LOCK_PATH);
145
+ }
146
+ catch {
147
+ try {
148
+ unlinkSync(tmpLock);
149
+ }
150
+ catch { /* cleanup */ }
151
+ return false;
152
+ }
153
+ // Verify we own the lock (another process may have renamed over us).
154
+ try {
155
+ const owner = readFileSync(LOCK_PATH, "utf-8").split("\n")[0];
156
+ if (owner !== String(process.pid))
157
+ return false;
158
+ }
159
+ catch {
160
+ return false;
161
+ }
127
162
  return true;
128
163
  }
129
- catch (err) {
130
- if (err?.code === "EEXIST")
131
- return false;
164
+ catch {
132
165
  return false;
133
166
  }
134
167
  }
package/dist/server.js CHANGED
@@ -76,6 +76,7 @@ import { verifyBehaviorHandler } from "./tools/behavioralVerifierHandler.js";
76
76
  import { getStorage } from "./storage/index.js";
77
77
  import { getSettingSync, initConfigStorage } from "./storage/configStorage.js";
78
78
  import { sanitizeMcpOutput } from "./utils/sanitizer.js";
79
+ import { sanitizeForLog } from "./utils/logger.js";
79
80
  import { getTracer, initTelemetry } from "./utils/telemetry.js";
80
81
  import { context as otelContext, trace, SpanStatusCode } from "@opentelemetry/api";
81
82
  import { ddInfo, ddError as ddLogError } from "./utils/ddLogger.js";
@@ -1020,7 +1021,7 @@ export function createServer() {
1020
1021
  catch (error) {
1021
1022
  const errMsg = error instanceof Error ? error.message : String(error);
1022
1023
  const _ddErrDuration = Date.now() - _ddStart;
1023
- console.error(`Error in tool handler: ${errMsg}`);
1024
+ console.error(`Error in tool handler: ${sanitizeForLog(errMsg)}`);
1024
1025
  ddLogError("mcp.tool.error", error instanceof Error ? error : undefined, { tool: name, project: args?.project, durationMs: _ddErrDuration });
1025
1026
  rootSpan.recordException(error instanceof Error ? error : new Error(errMsg));
1026
1027
  rootSpan.setStatus({
@@ -2,6 +2,7 @@ import { createClient } from "@libsql/client";
2
2
  import { resolve, dirname } from "path";
3
3
  import { homedir } from "os";
4
4
  import { existsSync, mkdirSync } from "fs";
5
+ const PROTO_KEYS = new Set(["__proto__", "constructor", "prototype"]);
5
6
  // We use a small, dedicated DB just for configuration settings.
6
7
  // This solves the chicken-and-egg problem: we need to know WHICH
7
8
  // storage backend to boot *before* we can use that backend.
@@ -22,7 +23,7 @@ let initialized = false;
22
23
  // (e.g. ReadResourceRequestSchema) can read settings synchronously
23
24
  // without opening an additional SQLite round-trip and stalling the
24
25
  // MCP stdio handshake (which causes a black-screen on startup).
25
- let settingsCache = null;
26
+ let settingsCache = null; // assigned as Object.create(null) below
26
27
  function getClient() {
27
28
  if (!configClient) {
28
29
  // Ensure the directory exists before opening the DB.
@@ -52,9 +53,12 @@ export async function initConfigStorage() {
52
53
  `);
53
54
  // Preload all rows into the cache so subsequent reads are zero-cost.
54
55
  const rs = await client.execute("SELECT key, value FROM system_settings");
55
- settingsCache = {};
56
+ settingsCache = Object.create(null);
57
+ const cache = settingsCache;
56
58
  for (const row of rs.rows) {
57
- settingsCache[row.key] = row.value;
59
+ const k = row.key;
60
+ if (!PROTO_KEYS.has(k))
61
+ cache[k] = row.value;
58
62
  }
59
63
  }
60
64
  catch (err) {
@@ -63,7 +67,7 @@ export async function initConfigStorage() {
63
67
  // getSettingSync() will return defaults; getSetting()/setSetting()
64
68
  // will attempt to re-open the DB on first call.
65
69
  console.error(`[configStorage] Failed to initialize (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
66
- settingsCache = {};
70
+ settingsCache = Object.create(null);
67
71
  }
68
72
  initialized = true;
69
73
  }
@@ -93,7 +97,7 @@ export async function getSetting(key, defaultValue = "") {
93
97
  if (rs.rows.length > 0) {
94
98
  const value = rs.rows[0].value;
95
99
  // Populate cache entry for future reads.
96
- if (settingsCache)
100
+ if (settingsCache && !PROTO_KEYS.has(key))
97
101
  settingsCache[key] = value;
98
102
  return value;
99
103
  }
@@ -117,7 +121,7 @@ export async function setSetting(key, value) {
117
121
  args: [key, value],
118
122
  });
119
123
  // Keep the cache in sync so getSettingSync() reflects the new value immediately.
120
- if (settingsCache && typeof key === "string" && !["__proto__", "constructor", "prototype"].includes(key)) {
124
+ if (settingsCache && typeof key === "string" && !PROTO_KEYS.has(key)) {
121
125
  settingsCache[key] = value;
122
126
  }
123
127
  return; // Success — exit
@@ -142,9 +146,11 @@ export async function getAllSettings() {
142
146
  }
143
147
  const client = getClient();
144
148
  const rs = await client.execute("SELECT key, value FROM system_settings");
145
- const settings = {};
149
+ const settings = Object.create(null);
146
150
  for (const row of rs.rows) {
147
- settings[row.key] = row.value;
151
+ const k = row.key;
152
+ if (!PROTO_KEYS.has(k))
153
+ settings[k] = row.value;
148
154
  }
149
155
  return settings;
150
156
  }
@@ -46,7 +46,7 @@ export function applySentinelBlock(existingContent, rulesBlock) {
46
46
  return `${existingContent}${separator}${rulesBlock}\n`;
47
47
  }
48
48
  export function redactSettings(settings) {
49
- const redacted = {};
49
+ const redacted = Object.create(null);
50
50
  for (const [k, v] of Object.entries(settings || {})) {
51
51
  if (typeof k !== "string" || k === "__proto__" || k === "constructor" || k === "prototype")
52
52
  continue;
@@ -12,6 +12,7 @@
12
12
  * The handler is storage-agnostic — works with SQLite (local) or Supabase (remote).
13
13
  */
14
14
  import { readFileSync, existsSync } from "fs";
15
+ import { resolve as resolvePath } from "path";
15
16
  import { basename } from "path";
16
17
  import { PRISM_USER_ID } from "../config.js";
17
18
  import { getStorage } from "../storage/index.js";
@@ -103,7 +104,7 @@ export async function ingestKnowledge(args) {
103
104
  const { project, source_label, chunk_size = 4000, } = args;
104
105
  let content = args.content || "";
105
106
  if (args.file_path) {
106
- const resolved = require("path").resolve(args.file_path);
107
+ const resolved = resolvePath(args.file_path);
107
108
  const blocked = ["/etc", "/var", "/usr", "/sys", "/proc", "/dev", "/root",
108
109
  "/.ssh", "/.env", "/.git/config", "/private/etc"].some(p => resolved.startsWith(p) || resolved.includes("/."));
109
110
  if (blocked) {
@@ -1581,7 +1581,8 @@ export async function sessionExportMemoryHandler(args) {
1581
1581
  // Serialize
1582
1582
  const ext = format === "markdown" ? "md" : format === "vault" ? "zip" : "json";
1583
1583
  const safeProject = project.replace(/[^a-zA-Z0-9_-]/g, "_");
1584
- const filename = `prism-export-${safeProject}-${dateSuffix}.${ext}`;
1584
+ const token = randomUUID().slice(0, 8);
1585
+ const filename = `prism-export-${safeProject}-${dateSuffix}-${token}.${ext}`;
1585
1586
  const outputPath = join(output_dir, filename);
1586
1587
  let content;
1587
1588
  if (format === "vault") {
@@ -1611,7 +1612,7 @@ export async function sessionExportMemoryHandler(args) {
1611
1612
  const dot = filename.lastIndexOf(".");
1612
1613
  const stem = dot > 0 ? filename.slice(0, dot) : filename;
1613
1614
  const extPart = dot > 0 ? filename.slice(dot) : "";
1614
- finalPath = join(output_dir, `${stem}-${Date.now()}${extPart}`);
1615
+ finalPath = join(output_dir, `${stem}-${randomUUID()}${extPart}`);
1615
1616
  await writeFile(finalPath, content, { flag: "wx" });
1616
1617
  }
1617
1618
  else {
@@ -30,7 +30,7 @@ import * as fs from "fs";
30
30
  import * as nodePath from "path";
31
31
  import { getLLMProvider } from "./llm/factory.js";
32
32
  import { getStorage } from "../storage/index.js";
33
- import { debugLog } from "./logger.js";
33
+ import { debugLog, sanitizeForLog } from "./logger.js";
34
34
  import { PRISM_USER_ID } from "../config.js";
35
35
  import { getTracer } from "./telemetry.js";
36
36
  import { SpanStatusCode, context as otelContext, trace } from "@opentelemetry/api";
@@ -93,7 +93,7 @@ export function fireCaptionAsync(project, imageId, vaultPath, userContext) {
93
93
  code: SpanStatusCode.ERROR,
94
94
  message: err instanceof Error ? err.message : String(err),
95
95
  });
96
- console.error(`[ImageCaptioner] Failed for [${imageId}]: ${err instanceof Error ? err.message : String(err)}`);
96
+ console.error(`[ImageCaptioner] Failed for [${sanitizeForLog(imageId)}]: ${sanitizeForLog(err instanceof Error ? err.message : String(err))}`);
97
97
  })
98
98
  .finally(() => {
99
99
  // Always end the span — even on VLM failure — to flush the BatchSpanProcessor.
@@ -127,8 +127,8 @@ async function captionImageAsync(project, imageId, vaultPath, userContext) {
127
127
  if (fileSizeBytes > maxBytes) {
128
128
  const limitMB = (maxBytes / 1024 / 1024).toFixed(0);
129
129
  const actualMB = (fileSizeBytes / 1024 / 1024).toFixed(1);
130
- console.warn(`[ImageCaptioner] Image [${imageId}] is ${actualMB}MB, exceeding ` +
131
- `the ${textProvider} VLM limit (${limitMB}MB). Captioning skipped. ` +
130
+ console.warn(`[ImageCaptioner] Image [${sanitizeForLog(imageId)}] is ${sanitizeForLog(actualMB)}MB, exceeding ` +
131
+ `the ${sanitizeForLog(textProvider)} VLM limit (${limitMB}MB). Captioning skipped. ` +
132
132
  (textProvider === "anthropic"
133
133
  ? "Switch Embedding Provider to Gemini/OpenAI to caption larger images."
134
134
  : "Consider resizing the image."));
@@ -165,8 +165,8 @@ async function captionImageAsync(project, imageId, vaultPath, userContext) {
165
165
  // Don't poison the caption pipeline. The caption already succeeded;
166
166
  // OCR is additive value. Surface as warn (not error) so it's grep-able
167
167
  // but doesn't trip alerting on transient VLM blips.
168
- console.warn(`[ImageCaptioner] OCR failed for [${imageId}] (non-fatal): ` +
169
- `${ocrErr instanceof Error ? ocrErr.message : String(ocrErr)}`);
168
+ console.warn(`[ImageCaptioner] OCR failed for [${sanitizeForLog(imageId)}] (non-fatal): ` +
169
+ `${sanitizeForLog(ocrErr instanceof Error ? ocrErr.message : String(ocrErr))}`);
170
170
  }
171
171
  }
172
172
  // ── Step 4: Patch handoff visual_memory entry ─────────────────────────
@@ -282,6 +282,6 @@ async function updateHandoffCaption(project, imageId, caption, vaultPath, ocrTex
282
282
  // OCC conflict — retry once with fresh version
283
283
  debugLog(`[ImageCaptioner] OCC conflict patching [${imageId}], attempt ${attempt}. Retrying…`);
284
284
  }
285
- console.warn(`[ImageCaptioner] Could not patch handoff for [${imageId}] after 2 attempts. ` +
285
+ console.warn(`[ImageCaptioner] Could not patch handoff for [${sanitizeForLog(imageId)}] after 2 attempts. ` +
286
286
  `Caption is still saved in the ledger and will surface via semantic search.`);
287
287
  }
@@ -47,6 +47,7 @@ import { VoyageAdapter } from "./adapters/voyage.js";
47
47
  import { LocalEmbeddingAdapter } from "./adapters/local.js";
48
48
  import { DisabledTextAdapter } from "./adapters/disabledText.js";
49
49
  import { TracingLLMProvider } from "./adapters/traced.js";
50
+ import { sanitizeForLog } from "../logger.js";
50
51
  // Module-level singleton — one composed provider per MCP server process.
51
52
  let providerInstance = null;
52
53
  // ─── Adapter Builders ─────────────────────────────────────────────────────────
@@ -149,13 +150,13 @@ export function getLLMProvider() {
149
150
  // The text provider name is used as the primary span attribute label.
150
151
  providerInstance = new TracingLLMProvider(composed, textType);
151
152
  if (textType !== embedType) {
152
- console.info(`[LLMFactory] Split provider: text=${textType}, embedding=${embedType}`);
153
+ console.info(`[LLMFactory] Split provider: text=${sanitizeForLog(textType)}, embedding=${sanitizeForLog(embedType)}`);
153
154
  }
154
155
  }
155
156
  catch (err) {
156
157
  // Init failure (e.g. missing API key) → fall back to full Gemini provider.
157
158
  // A crash here would silently kill the MCP server.
158
- console.error(`[LLMFactory] Failed to initialise providers (text=${textType}, embed=${embedType}): ${err instanceof Error ? err.message : String(err)}. ` +
159
+ console.error(`[LLMFactory] Failed to initialise providers (text=${sanitizeForLog(textType)}, embed=${sanitizeForLog(embedType)}): ${sanitizeForLog(err instanceof Error ? err.message : String(err))}. ` +
159
160
  `Falling back to GeminiAdapter for both.`);
160
161
  const fallback = new GeminiAdapter();
161
162
  const fallbackComposed = {
@@ -4,11 +4,11 @@ import { PRISM_DEBUG_LOGGING } from "../config.js";
4
4
  * newlines, and ANSI escape sequences that could be used for log
5
5
  * injection or terminal escape attacks.
6
6
  */
7
- function sanitizeForLog(msg) {
7
+ export function sanitizeForLog(msg) {
8
8
  return msg
9
- .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") // control chars (keep \n \r \t)
9
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "") // C0 + C1 control chars (keep \n \r \t)
10
10
  .replace(/\r?\n/g, " ⏎ ") // newlines → visible marker
11
- .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ""); // ANSI escape sequences
11
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ""); // CSI residue (defense-in-depth if first regex relaxed)
12
12
  }
13
13
  /**
14
14
  * Logs a message to stderr only if PRISM_DEBUG_LOGGING is true.
@@ -1,3 +1,4 @@
1
+ import { openSync, readSync, closeSync } from "node:fs";
1
2
  /**
2
3
  * ═══════════════════════════════════════════════════════════════════
3
4
  * Migration Utilities — Shared Normalization Helpers
@@ -43,11 +44,10 @@
43
44
  * OpenAI → `"tool_calls":` or `"created_at":` (Unix epoch) or `"role":"system"`
44
45
  */
45
46
  export function sniffFormat(filePath) {
46
- const fs = require('node:fs');
47
- const fd = fs.openSync(filePath, 'r');
47
+ const fd = openSync(filePath, 'r');
48
48
  const buf = Buffer.alloc(4096);
49
- const bytesRead = fs.readSync(fd, buf, 0, 4096, 0);
50
- fs.closeSync(fd);
49
+ const bytesRead = readSync(fd, buf, 0, 4096, 0);
50
+ closeSync(fd);
51
51
  if (bytesRead === 0)
52
52
  return null;
53
53
  const head = buf.toString('utf8', 0, bytesRead);
@@ -61,6 +61,7 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
61
61
  import { resourceFromAttributes } from "@opentelemetry/resources";
62
62
  import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
63
63
  import { getSettingSync } from "../storage/configStorage.js";
64
+ import { sanitizeForLog } from "./logger.js";
64
65
  // ─── Module-level singleton ───────────────────────────────────────────────────
65
66
  // Null when OTel is disabled or before initTelemetry() is called.
66
67
  let _provider = null;
@@ -175,11 +176,11 @@ export function initTelemetry() {
175
176
  });
176
177
  _provider.register();
177
178
  console.error(`[Telemetry] OpenTelemetry initialized. ` +
178
- `Service: "${serviceName}", Endpoint: "${endpoint}"`);
179
+ `Service: "${sanitizeForLog(serviceName)}", Endpoint: "${sanitizeForLog(endpoint)}"`);
179
180
  }
180
181
  catch (err) {
181
182
  // OTel init errors must NEVER crash the server. Log and continue.
182
- console.error(`[Telemetry] Failed to initialize OTel (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
183
+ console.error(`[Telemetry] Failed to initialize OTel (non-fatal): ${sanitizeForLog(err instanceof Error ? err.message : String(err))}`);
183
184
  _provider = null; // Clean state so getTracer() returns no-op
184
185
  }
185
186
  }
@@ -226,6 +227,6 @@ export async function shutdownTelemetry() {
226
227
  }
227
228
  catch (err) {
228
229
  // Log but don't rethrow — shutdown errors must not prevent DB cleanup.
229
- console.error(`[Telemetry] Error during shutdown (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
230
+ console.error(`[Telemetry] Error during shutdown (non-fatal): ${sanitizeForLog(err instanceof Error ? err.message : String(err))}`);
230
231
  }
231
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "19.2.6",
3
+ "version": "19.2.8",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
5
  "description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
6
6
  "module": "index.ts",