prism-mcp-server 3.1.1 → 4.2.0
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 +344 -111
- package/dist/config.js +8 -0
- package/dist/dashboard/ui.js +120 -0
- package/dist/lifecycle.js +164 -0
- package/dist/server.js +117 -46
- package/dist/storage/configStorage.js +44 -11
- package/dist/storage/sqlite.js +94 -2
- package/dist/storage/supabase.js +35 -0
- package/dist/storage/supabaseMigrations.js +110 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +111 -3
- package/dist/tools/sessionMemoryHandlers.js +155 -5
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -114,6 +114,14 @@ export const PRISM_DEBUG_LOGGING = process.env.PRISM_DEBUG_LOGGING === "true";
|
|
|
114
114
|
// doesn't increase tool count.
|
|
115
115
|
// Set PRISM_ENABLE_HIVEMIND=true to unlock the Agent Registry tools.
|
|
116
116
|
export const PRISM_ENABLE_HIVEMIND = process.env.PRISM_ENABLE_HIVEMIND === "true";
|
|
117
|
+
// ─── v4.1: Auto-Load Projects ────────────────────────────────
|
|
118
|
+
// Auto-load is configured exclusively via the Mind Palace dashboard
|
|
119
|
+
// ("Auto-Load Projects" checkboxes in Settings). The setting is stored
|
|
120
|
+
// in prism-config.db and read at startup via getSettingSync().
|
|
121
|
+
//
|
|
122
|
+
// The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
|
|
123
|
+
// is the single source of truth. This prevents mismatches between
|
|
124
|
+
// env var and dashboard values causing duplicate project loads.
|
|
117
125
|
if (PRISM_AUTO_CAPTURE) {
|
|
118
126
|
// Use console.error instead of debugLog here to prevent circular dependency
|
|
119
127
|
if (PRISM_DEBUG_LOGGING) {
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -696,6 +696,19 @@ export function renderDashboardHTML(version) {
|
|
|
696
696
|
</select>
|
|
697
697
|
</div>
|
|
698
698
|
|
|
699
|
+
<div class="setting-row">
|
|
700
|
+
<div>
|
|
701
|
+
<div class="setting-label">Token Budget</div>
|
|
702
|
+
<div class="setting-desc">Max tokens for session_load_context (0 = unlimited)</div>
|
|
703
|
+
</div>
|
|
704
|
+
<input type="number" id="input-max-tokens"
|
|
705
|
+
placeholder="0"
|
|
706
|
+
min="0" max="100000" step="500"
|
|
707
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 90px; text-align: right;"
|
|
708
|
+
onchange="saveSetting('max_tokens', this.value)"
|
|
709
|
+
oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('max_tokens',this.value),800)" />
|
|
710
|
+
</div>
|
|
711
|
+
|
|
699
712
|
<div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
|
|
700
713
|
|
|
701
714
|
<div class="setting-row">
|
|
@@ -716,6 +729,26 @@ export function renderDashboardHTML(version) {
|
|
|
716
729
|
</select>
|
|
717
730
|
</div>
|
|
718
731
|
|
|
732
|
+
<div class="setting-row" style="align-items:flex-start">
|
|
733
|
+
<div>
|
|
734
|
+
<div class="setting-label">Auto-Load Projects</div>
|
|
735
|
+
<div class="setting-desc">Select projects to auto-push context on startup</div>
|
|
736
|
+
</div>
|
|
737
|
+
<div id="autoload-checkboxes" style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;font-family:var(--font-mono);max-height:120px;overflow-y:auto;">
|
|
738
|
+
<span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
<div class="setting-row" style="align-items:flex-start">
|
|
743
|
+
<div>
|
|
744
|
+
<div class="setting-label">Project Repo Paths</div>
|
|
745
|
+
<div class="setting-desc">Map each project to its repo directory for save validation</div>
|
|
746
|
+
</div>
|
|
747
|
+
<div id="repopath-inputs" style="display:flex;flex-direction:column;gap:6px;font-size:0.85rem;max-height:160px;overflow-y:auto;">
|
|
748
|
+
<span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
719
752
|
<div class="setting-section">Agent Identity</div>
|
|
720
753
|
|
|
721
754
|
<div class="setting-row">
|
|
@@ -1278,6 +1311,88 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
1278
1311
|
}
|
|
1279
1312
|
|
|
1280
1313
|
|
|
1314
|
+
// ─── Auto-Load Checkboxes (v4.1) ─────────────────────────────────
|
|
1315
|
+
async function loadAutoloadCheckboxes() {
|
|
1316
|
+
var container = document.getElementById('autoload-checkboxes');
|
|
1317
|
+
if (!container) return;
|
|
1318
|
+
try {
|
|
1319
|
+
var projRes = await fetch('/api/projects');
|
|
1320
|
+
var projData = await projRes.json();
|
|
1321
|
+
var projects = projData.projects || [];
|
|
1322
|
+
|
|
1323
|
+
var settRes = await fetch('/api/settings');
|
|
1324
|
+
var settData = await settRes.json();
|
|
1325
|
+
var saved = (settData.settings || {}).autoload_projects || '';
|
|
1326
|
+
var selected = saved.split(',').map(function(s){ return s.trim(); }).filter(Boolean);
|
|
1327
|
+
|
|
1328
|
+
if (projects.length === 0) {
|
|
1329
|
+
container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
container.innerHTML = projects.map(function(p) {
|
|
1334
|
+
var checked = selected.indexOf(p) !== -1 ? ' checked' : '';
|
|
1335
|
+
return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text-primary)">' +
|
|
1336
|
+
'<input type="checkbox" value="' + escapeHtml(p) + '"' + checked +
|
|
1337
|
+
' onchange="onAutoloadToggle()"' +
|
|
1338
|
+
' style="accent-color:var(--accent-purple);cursor:pointer" />' +
|
|
1339
|
+
escapeHtml(p) + '</label>';
|
|
1340
|
+
}).join('');
|
|
1341
|
+
} catch(e) {
|
|
1342
|
+
container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function onAutoloadToggle() {
|
|
1347
|
+
var container = document.getElementById('autoload-checkboxes');
|
|
1348
|
+
if (!container) return;
|
|
1349
|
+
var boxes = container.querySelectorAll('input[type=checkbox]');
|
|
1350
|
+
var selected = [];
|
|
1351
|
+
for (var i = 0; i < boxes.length; i++) {
|
|
1352
|
+
if (boxes[i].checked) selected.push(boxes[i].value);
|
|
1353
|
+
}
|
|
1354
|
+
saveBootSetting('autoload_projects', selected.join(','));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// ─── Project Repo Paths (v4.2) ─────────────────────────────────
|
|
1358
|
+
async function loadRepoPathInputs() {
|
|
1359
|
+
var container = document.getElementById('repopath-inputs');
|
|
1360
|
+
if (!container) return;
|
|
1361
|
+
try {
|
|
1362
|
+
var projRes = await fetch('/api/projects');
|
|
1363
|
+
var projData = await projRes.json();
|
|
1364
|
+
var projects = projData.projects || [];
|
|
1365
|
+
|
|
1366
|
+
var settRes = await fetch('/api/settings');
|
|
1367
|
+
var settData = await settRes.json();
|
|
1368
|
+
var settings = settData.settings || {};
|
|
1369
|
+
|
|
1370
|
+
if (projects.length === 0) {
|
|
1371
|
+
container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
container.innerHTML = projects.map(function(p) {
|
|
1376
|
+
var savedPath = settings['repo_path:' + p] || '';
|
|
1377
|
+
return '<div style="display:flex;align-items:center;gap:6px">' +
|
|
1378
|
+
'<span style="min-width:100px;color:var(--text-secondary);font-family:var(--font-mono);font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml(p) + '">' + escapeHtml(p) + '</span>' +
|
|
1379
|
+
'<input type="text" value="' + escapeHtml(savedPath) + '"' +
|
|
1380
|
+
' placeholder="/path/to/repo"' +
|
|
1381
|
+
' data-project="' + escapeHtml(p) + '"' +
|
|
1382
|
+
' style="flex:1;min-width:140px;padding:0.2rem 0.4rem;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border-glass);border-radius:4px;font-size:0.8rem;font-family:var(--font-mono)"' +
|
|
1383
|
+
' onchange="saveRepoPath(this.dataset.project, this.value)"' +
|
|
1384
|
+
' oninput="clearTimeout(this._t); var self=this; this._t=setTimeout(function(){saveRepoPath(self.dataset.project, self.value)},1200)" />' +
|
|
1385
|
+
'</div>';
|
|
1386
|
+
}).join('');
|
|
1387
|
+
} catch(e) {
|
|
1388
|
+
container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async function saveRepoPath(project, path) {
|
|
1393
|
+
await saveSetting('repo_path:' + project, path.trim());
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1281
1396
|
async function loadSettings() {
|
|
1282
1397
|
try {
|
|
1283
1398
|
var res = await fetch('/api/settings');
|
|
@@ -1304,6 +1419,11 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
1304
1419
|
// Agent Identity
|
|
1305
1420
|
if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
|
|
1306
1421
|
if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
|
|
1422
|
+
if (s.max_tokens) document.getElementById('input-max-tokens').value = s.max_tokens;
|
|
1423
|
+
// Autoload checkboxes are loaded dynamically
|
|
1424
|
+
loadAutoloadCheckboxes();
|
|
1425
|
+
// Repo path inputs are loaded dynamically
|
|
1426
|
+
loadRepoPathInputs();
|
|
1307
1427
|
} catch(e) { console.warn('Settings load failed:', e); }
|
|
1308
1428
|
}
|
|
1309
1429
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Lifecycle Management
|
|
3
|
+
* Handles singleton PID locking, graceful shutdown, and SQLite handle cleanup.
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: All logging MUST use console.error() (stderr).
|
|
6
|
+
* Using console.log() (stdout) will corrupt the MCP JSON-RPC stream.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { closeConfigStorage } from "./storage/configStorage.js";
|
|
13
|
+
import { getStorage } from "./storage/index.js";
|
|
14
|
+
const PRISM_DIR = path.join(os.homedir(), ".prism-mcp");
|
|
15
|
+
/**
|
|
16
|
+
* Instance-aware PID file.
|
|
17
|
+
* Set PRISM_INSTANCE env var to run multiple Prism MCP servers
|
|
18
|
+
* side-by-side (e.g. "athena-public" and "prism-mcp").
|
|
19
|
+
* Each instance gets its own PID file to prevent lock conflicts.
|
|
20
|
+
*/
|
|
21
|
+
const INSTANCE_NAME = process.env.PRISM_INSTANCE || "default";
|
|
22
|
+
const PID_FILE = path.join(PRISM_DIR, `server-${INSTANCE_NAME}.pid`);
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.error(`[Prism Lifecycle] ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a process is an orphan (adopted by init/launchd, PPID=1).
|
|
28
|
+
* Returns false on Windows (PID logic is different there).
|
|
29
|
+
*/
|
|
30
|
+
function isOrphanProcess(pid) {
|
|
31
|
+
if (process.platform === "win32") {
|
|
32
|
+
// Windows doesn't have reliable PPID checks via 'ps'.
|
|
33
|
+
// Safer to assume it's NOT an orphan to avoid killing valid instances.
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// 'ps -o ppid= -p PID' returns just the parent PID
|
|
38
|
+
const ppid = execSync(`ps -o ppid= -p ${pid}`, { encoding: "utf8" }).trim();
|
|
39
|
+
return ppid === "1";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// If ps fails (e.g. process gone), assume it's safe to claim
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ensures valid server execution state.
|
|
48
|
+
*
|
|
49
|
+
* LOGIC:
|
|
50
|
+
* 1. If --no-lock is passed, skip everything (testing mode).
|
|
51
|
+
* 2. If PID file exists:
|
|
52
|
+
* - If process is dead: Overwrite lock.
|
|
53
|
+
* - If process is alive AND is an orphan (PPID=1): Kill it (Zombie), then overwrite.
|
|
54
|
+
* - If process is alive AND has a parent: Log warning, allow coexistence (don't kill).
|
|
55
|
+
*/
|
|
56
|
+
export function acquireLock() {
|
|
57
|
+
if (process.argv.includes("--no-lock")) {
|
|
58
|
+
log("Lock acquisition skipped (--no-lock flag)");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(PRISM_DIR)) {
|
|
62
|
+
fs.mkdirSync(PRISM_DIR, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
if (fs.existsSync(PID_FILE)) {
|
|
65
|
+
try {
|
|
66
|
+
const oldPid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
67
|
+
if (oldPid && oldPid !== process.pid) {
|
|
68
|
+
let isAlive = false;
|
|
69
|
+
try {
|
|
70
|
+
process.kill(oldPid, 0); // 0 signal checks for existence
|
|
71
|
+
isAlive = true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
isAlive = false;
|
|
75
|
+
}
|
|
76
|
+
if (isAlive) {
|
|
77
|
+
// Process exists. Is it a zombie?
|
|
78
|
+
if (isOrphanProcess(oldPid)) {
|
|
79
|
+
log(`Found zombie process (PID ${oldPid}, PPID=1). Terminating...`);
|
|
80
|
+
try {
|
|
81
|
+
process.kill(oldPid, "SIGTERM");
|
|
82
|
+
// Give it 100ms to die, then force kill if needed
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(oldPid, "SIGKILL");
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}, 100);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
log(`Failed to kill zombie: ${e}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// It has a parent (e.g., another VS Code window or Claude Desktop)
|
|
96
|
+
log(`Existing server (PID ${oldPid}) is active and managed. Coexisting...`);
|
|
97
|
+
// We do NOT overwrite the PID file here.
|
|
98
|
+
// If we overwrite it, the *active* server will fail to clean up
|
|
99
|
+
// the PID file when it eventually shuts down.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
log(`Warning: Failed to process existing PID file: ${err}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Claim the lock for this process
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(PID_FILE, process.pid.toString(), "utf8");
|
|
112
|
+
log(`Acquired singleton lock (PID ${process.pid})`);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
log(`Warning: Failed to write PID file: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Registers handlers to close SQLite file handles cleanly when the server stops.
|
|
120
|
+
*/
|
|
121
|
+
export function registerShutdownHandlers() {
|
|
122
|
+
let shuttingDown = false;
|
|
123
|
+
const shutdown = async (reason) => {
|
|
124
|
+
if (shuttingDown)
|
|
125
|
+
return;
|
|
126
|
+
shuttingDown = true;
|
|
127
|
+
log(`Shutting down gracefully (${reason})...`);
|
|
128
|
+
try {
|
|
129
|
+
// 1. Close system settings DB
|
|
130
|
+
closeConfigStorage();
|
|
131
|
+
// 2. Close main ledger DB
|
|
132
|
+
const storage = await getStorage();
|
|
133
|
+
if (storage && typeof storage.close === "function") {
|
|
134
|
+
await storage.close();
|
|
135
|
+
}
|
|
136
|
+
// 3. Remove PID lockfile (only if WE own it)
|
|
137
|
+
if (fs.existsSync(PID_FILE)) {
|
|
138
|
+
try {
|
|
139
|
+
const storedPid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
140
|
+
if (storedPid === process.pid) {
|
|
141
|
+
fs.unlinkSync(PID_FILE);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ignore read errors during shutdown
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
log(`Error during shutdown cleanup: ${err}`);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// OS Signals
|
|
157
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
158
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
159
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
160
|
+
// MCP Client Disconnect (CRITICAL)
|
|
161
|
+
process.stdin.on("close", () => {
|
|
162
|
+
shutdown("CLIENT_DISCONNECTED_STDIN_CLOSED");
|
|
163
|
+
});
|
|
164
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -61,6 +61,7 @@ SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/
|
|
|
61
61
|
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND } from "./config.js";
|
|
62
62
|
import { getSyncBus } from "./sync/factory.js";
|
|
63
63
|
import { startDashboardServer } from "./dashboard/server.js";
|
|
64
|
+
import { acquireLock, registerShutdownHandlers } from "./lifecycle.js";
|
|
64
65
|
// ─── v2.3.6 FIX: Use Storage Abstraction for Prompts/Resources ───
|
|
65
66
|
// CRITICAL FIX: Previously imported supabaseRpc/supabaseGet directly,
|
|
66
67
|
// which bypassed the storage abstraction layer and caused the server
|
|
@@ -84,7 +85,9 @@ SESSION_HEALTH_CHECK_TOOL,
|
|
|
84
85
|
// ─── Phase 2: GDPR Memory Deletion tool definition ───
|
|
85
86
|
SESSION_FORGET_MEMORY_TOOL,
|
|
86
87
|
// ─── v3.1: TTL Retention tool ───
|
|
87
|
-
KNOWLEDGE_SET_RETENTION_TOOL,
|
|
88
|
+
KNOWLEDGE_SET_RETENTION_TOOL,
|
|
89
|
+
// v4.0: Active Behavioral Memory tools
|
|
90
|
+
SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
|
|
88
91
|
// ─── v0.4.0: New tool handlers ───
|
|
89
92
|
compactLedgerHandler, sessionSearchMemoryHandler,
|
|
90
93
|
// ─── v2.0: Time Travel handlers ───
|
|
@@ -97,6 +100,8 @@ sessionHealthCheckHandler,
|
|
|
97
100
|
sessionForgetMemoryHandler,
|
|
98
101
|
// ─── v3.1: TTL Retention handler ───
|
|
99
102
|
knowledgeSetRetentionHandler,
|
|
103
|
+
// v4.0: Active Behavioral Memory handlers
|
|
104
|
+
sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler,
|
|
100
105
|
// ─── v3.0: Agent Hivemind tools ───
|
|
101
106
|
AGENT_REGISTRY_TOOLS, agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler, } from "./tools/index.js";
|
|
102
107
|
// ─── Dynamic Tool Registration ───────────────────────────────────
|
|
@@ -110,39 +115,52 @@ const BASE_TOOLS = [
|
|
|
110
115
|
BRAVE_ANSWERS_TOOL, // brave_answers — AI-grounded answers
|
|
111
116
|
RESEARCH_PAPER_ANALYSIS_TOOL, // gemini_research_paper_analysis — paper analysis
|
|
112
117
|
];
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
118
|
+
// ─── v4.1 FIX: Build Session Memory Tools dynamically ────────
|
|
119
|
+
// The session_load_context tool description is dynamically modified
|
|
120
|
+
// to include auto-load project instructions. Tool descriptions are
|
|
121
|
+
// the ONLY mechanism guaranteed by ALL MCP clients (Antigravity,
|
|
122
|
+
// Claude Code, Claude CLI). The `instructions` field and
|
|
123
|
+
// sendLoggingMessage are NOT reliably surfaced by any client.
|
|
124
|
+
//
|
|
125
|
+
// buildSessionMemoryTools() is called inside createServer() AFTER
|
|
126
|
+
// initConfigStorage() has run, so getSettingSync() returns real
|
|
127
|
+
// dashboard config values.
|
|
128
|
+
function buildSessionMemoryTools(autoloadList) {
|
|
129
|
+
// Dynamically inject auto-load instruction into session_load_context description
|
|
130
|
+
let loadContextTool = SESSION_LOAD_CONTEXT_TOOL;
|
|
131
|
+
if (autoloadList.length > 0) {
|
|
132
|
+
const projectList = autoloadList.join(", ");
|
|
133
|
+
loadContextTool = {
|
|
134
|
+
...SESSION_LOAD_CONTEXT_TOOL,
|
|
135
|
+
description: SESSION_LOAD_CONTEXT_TOOL.description +
|
|
136
|
+
`\n\nIMPORTANT: At the start of every conversation, you MUST call this tool for each of these projects: ${projectList}. Do this before any other work. Load level should be "standard" unless the user specifies otherwise.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
SESSION_SAVE_LEDGER_TOOL, // session_save_ledger — append immutable session log
|
|
141
|
+
SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
|
|
142
|
+
loadContextTool, // session_load_context — progressive context loading (+ auto-load instruction)
|
|
143
|
+
KNOWLEDGE_SEARCH_TOOL, // knowledge_search — search accumulated knowledge
|
|
144
|
+
KNOWLEDGE_FORGET_TOOL, // knowledge_forget — prune bad/old memories
|
|
145
|
+
SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
|
|
146
|
+
SESSION_SEARCH_MEMORY_TOOL, // session_search_memory — semantic search via embeddings (v0.4.0)
|
|
147
|
+
MEMORY_HISTORY_TOOL, // memory_history — view version timeline (v2.0)
|
|
148
|
+
MEMORY_CHECKOUT_TOOL, // memory_checkout — revert to past version (v2.0)
|
|
149
|
+
// ─── v2.0: Visual Memory tools ───
|
|
150
|
+
SESSION_SAVE_IMAGE_TOOL, // session_save_image — save image to media vault (v2.0)
|
|
151
|
+
SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
|
|
152
|
+
// ─── v2.2.0: Health Check tool ───
|
|
153
|
+
SESSION_HEALTH_CHECK_TOOL, // session_health_check — brain integrity checker (v2.2.0)
|
|
154
|
+
// ─── Phase 2: GDPR Memory Deletion tool ───
|
|
155
|
+
SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
|
|
156
|
+
// ─── v3.1: TTL Retention tool ───
|
|
157
|
+
KNOWLEDGE_SET_RETENTION_TOOL, // knowledge_set_retention — set auto-expiry TTL for a project
|
|
158
|
+
// ─── v4.0: Active Behavioral Memory tools ───
|
|
159
|
+
SESSION_SAVE_EXPERIENCE_TOOL, // session_save_experience — record typed experience events
|
|
160
|
+
KNOWLEDGE_UPVOTE_TOOL, // knowledge_upvote — increase entry importance
|
|
161
|
+
KNOWLEDGE_DOWNVOTE_TOOL, // knowledge_downvote — decrease entry importance
|
|
162
|
+
];
|
|
163
|
+
}
|
|
146
164
|
// ─── v0.4.0: Resource Subscription Tracking ──────────────────────
|
|
147
165
|
// REVIEWER NOTE: We track which project URIs clients have subscribed
|
|
148
166
|
// to. When sessionSaveHandoffHandler successfully updates a project,
|
|
@@ -186,6 +204,35 @@ export function notifyResourceUpdate(project, server) {
|
|
|
186
204
|
* with subscribe support for live refresh
|
|
187
205
|
*/
|
|
188
206
|
export function createServer() {
|
|
207
|
+
// ─── v4.1 FIX: Auto-Load via Dynamic Tool Descriptions ────────
|
|
208
|
+
// Read auto-load projects EXCLUSIVELY from dashboard config
|
|
209
|
+
// (available after initConfigStorage() in startServer).
|
|
210
|
+
//
|
|
211
|
+
// ARCHITECTURE DECISION: We inject the auto-load instruction into
|
|
212
|
+
// the session_load_context TOOL DESCRIPTION, not into `instructions`
|
|
213
|
+
// or `sendLoggingMessage`. Tool descriptions are the ONLY mechanism
|
|
214
|
+
// guaranteed by ALL MCP clients (Antigravity, Claude Code, Claude CLI).
|
|
215
|
+
//
|
|
216
|
+
// The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
|
|
217
|
+
// is the single source of truth. This prevents mismatches between
|
|
218
|
+
// env var and dashboard settings causing duplicate project loads.
|
|
219
|
+
const dashboardAutoload = getSettingSync("autoload_projects", "");
|
|
220
|
+
const autoloadList = dashboardAutoload
|
|
221
|
+
.split(",").map(p => p.trim()).filter(Boolean);
|
|
222
|
+
if (autoloadList.length > 0) {
|
|
223
|
+
console.error(`[Prism] Auto-load projects (dashboard): ${autoloadList.join(', ')}`);
|
|
224
|
+
}
|
|
225
|
+
// Build the dynamic tool list with auto-load instruction injected
|
|
226
|
+
const SESSION_MEMORY_TOOLS = buildSessionMemoryTools(autoloadList);
|
|
227
|
+
// Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
|
|
228
|
+
// can enumerate the full capability set. Runtime guards in the CallTool handler
|
|
229
|
+
// still prevent execution without valid Supabase credentials.
|
|
230
|
+
const ALL_TOOLS = [
|
|
231
|
+
...BASE_TOOLS,
|
|
232
|
+
...SESSION_MEMORY_TOOLS,
|
|
233
|
+
// v3.0: Agent Hivemind tools — only when PRISM_ENABLE_HIVEMIND=true
|
|
234
|
+
...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
|
|
235
|
+
];
|
|
189
236
|
const server = new Server({
|
|
190
237
|
name: SERVER_CONFIG.name,
|
|
191
238
|
version: SERVER_CONFIG.version,
|
|
@@ -193,21 +240,13 @@ export function createServer() {
|
|
|
193
240
|
capabilities: {
|
|
194
241
|
tools: {},
|
|
195
242
|
// ─── v0.4.0: Prompt capability (Enhancement #1) ───
|
|
196
|
-
// REVIEWER NOTE: Declaring `prompts: {}` tells Claude Desktop
|
|
197
|
-
// that we support the prompts/list and prompts/get methods.
|
|
198
|
-
// This enables the /resume_session slash command in the UI.
|
|
199
|
-
// Only enabled when Supabase is configured (prompts need
|
|
200
|
-
// session data to be useful).
|
|
201
243
|
...(SESSION_MEMORY_ENABLED ? { prompts: {} } : {}),
|
|
202
244
|
// ─── v0.4.0: Resource capability (Enhancement #3) ───
|
|
203
|
-
// REVIEWER NOTE: Setting subscribe: true tells Claude Desktop
|
|
204
|
-
// that we support resource subscriptions. When a user attaches
|
|
205
|
-
// memory://project/handoff and the LLM later updates it,
|
|
206
|
-
// we push an update notification so the attached context
|
|
207
|
-
// is silently refreshed. Without subscribe:true, the
|
|
208
|
-
// paperclipped context would become stale.
|
|
209
245
|
...(SESSION_MEMORY_ENABLED ? { resources: { subscribe: true } } : {}),
|
|
210
246
|
},
|
|
247
|
+
// Supplementary signal — not all clients support this field.
|
|
248
|
+
// Primary mechanism is the dynamic tool description above.
|
|
249
|
+
instructions: `Prism MCP — The Mind Palace for AI Agents. This server provides persistent session memory, knowledge search, and context management tools. Use session_load_context to recover previous work state, session_save_ledger to log completed work, and session_save_handoff to preserve state for the next session.`,
|
|
211
250
|
});
|
|
212
251
|
// ── Handler: Initialize ──
|
|
213
252
|
// NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
|
|
@@ -586,6 +625,19 @@ export function createServer() {
|
|
|
586
625
|
if (!SESSION_MEMORY_ENABLED)
|
|
587
626
|
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
588
627
|
return await knowledgeSetRetentionHandler(args);
|
|
628
|
+
// ─── v4.0: Active Behavioral Memory Tools ───
|
|
629
|
+
case "session_save_experience":
|
|
630
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
631
|
+
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
632
|
+
return await sessionSaveExperienceHandler(args);
|
|
633
|
+
case "knowledge_upvote":
|
|
634
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
635
|
+
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
636
|
+
return await knowledgeUpvoteHandler(args);
|
|
637
|
+
case "knowledge_downvote":
|
|
638
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
639
|
+
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
640
|
+
return await knowledgeDownvoteHandler(args);
|
|
589
641
|
// ─── v3.0: Agent Hivemind Tools ───
|
|
590
642
|
case "agent_register":
|
|
591
643
|
if (!SESSION_MEMORY_ENABLED)
|
|
@@ -645,7 +697,7 @@ export function createSandboxServer() {
|
|
|
645
697
|
});
|
|
646
698
|
// Register all tool listings unconditionally
|
|
647
699
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
648
|
-
tools: [...BASE_TOOLS, ...
|
|
700
|
+
tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS],
|
|
649
701
|
}));
|
|
650
702
|
// Register prompts listing so scanners see resume_session
|
|
651
703
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
@@ -694,6 +746,9 @@ export function createSandboxServer() {
|
|
|
694
746
|
* responses to stdout. Log messages go to stderr.
|
|
695
747
|
*/
|
|
696
748
|
export async function startServer() {
|
|
749
|
+
// MUST BE FIRST: Kill any zombie processes and acquire the singleton PID lock
|
|
750
|
+
// before touching SQLite. This prevents lock contention on prism-config.db.
|
|
751
|
+
acquireLock();
|
|
697
752
|
// Pre-warm the config settings cache BEFORE connecting the MCP transport.
|
|
698
753
|
// This ensures getSettingSync() returns real values (agent_name, default_role)
|
|
699
754
|
// during the Initialize handshake — zero extra latency for resource reads.
|
|
@@ -702,6 +757,10 @@ export async function startServer() {
|
|
|
702
757
|
const server = createServer();
|
|
703
758
|
const transport = new StdioServerTransport();
|
|
704
759
|
await server.connect(transport);
|
|
760
|
+
// Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
|
|
761
|
+
// The stdin close handler is critical — when MCP clients disconnect, they
|
|
762
|
+
// often just close the pipe without sending a signal, leaving zombie processes.
|
|
763
|
+
registerShutdownHandlers();
|
|
705
764
|
// Pre-warm storage AFTER connecting — fired async so we never block the
|
|
706
765
|
// stdio handshake. Supabase REST initialization can take 500ms–5s; blocking
|
|
707
766
|
// on it before server.connect() was the root cause of the 1m 56s CLI delay.
|
|
@@ -719,6 +778,18 @@ export async function startServer() {
|
|
|
719
778
|
]).catch(err => {
|
|
720
779
|
console.error(`[Prism] Storage pre-warm failed (non-fatal): ${err}`);
|
|
721
780
|
});
|
|
781
|
+
// ─── v4.1: Auto-Load is handled via dynamic tool descriptions ──
|
|
782
|
+
// The session_load_context tool description is dynamically modified
|
|
783
|
+
// in createServer() → buildSessionMemoryTools() to include the
|
|
784
|
+
// auto-load projects list. This is the ONLY universally reliable
|
|
785
|
+
// mechanism — tool descriptions are surfaced by ALL MCP clients.
|
|
786
|
+
//
|
|
787
|
+
// Previous approaches that FAILED:
|
|
788
|
+
// - sendLoggingMessage: goes to debug logs, not AI conversation
|
|
789
|
+
// - instructions field: not supported by Claude Code or Claude CLI
|
|
790
|
+
//
|
|
791
|
+
// No runtime code needed here — the instruction is baked into the
|
|
792
|
+
// tool schema returned by ListTools.
|
|
722
793
|
}
|
|
723
794
|
// ─── v2.0 Step 6: Initialize SyncBus (Telepathy) ───
|
|
724
795
|
// Fire-and-forget — SyncBus is non-critical for startup.
|
|
@@ -102,17 +102,36 @@ export async function getSetting(key, defaultValue = "") {
|
|
|
102
102
|
export async function setSetting(key, value) {
|
|
103
103
|
await initConfigStorage();
|
|
104
104
|
const client = getClient();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
// Retry with exponential backoff for SQLITE_BUSY (concurrent writes).
|
|
106
|
+
// The dashboard and load tests can fire many parallel setting saves.
|
|
107
|
+
const MAX_RETRIES = 5;
|
|
108
|
+
const BASE_DELAY_MS = 20;
|
|
109
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
await client.execute({
|
|
112
|
+
sql: `
|
|
113
|
+
INSERT INTO system_settings (key, value, updated_at)
|
|
114
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
115
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
116
|
+
`,
|
|
117
|
+
args: [key, value],
|
|
118
|
+
});
|
|
119
|
+
// Keep the cache in sync so getSettingSync() reflects the new value immediately.
|
|
120
|
+
if (settingsCache) {
|
|
121
|
+
settingsCache[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return; // Success — exit
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const isBusy = err?.code === "SQLITE_BUSY" || err?.rawCode === 5;
|
|
127
|
+
if (isBusy && attempt < MAX_RETRIES) {
|
|
128
|
+
// Exponential backoff + jitter
|
|
129
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 10;
|
|
130
|
+
await new Promise(r => setTimeout(r, delay));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw err; // Not SQLITE_BUSY or retries exhausted
|
|
134
|
+
}
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
export async function getAllSettings() {
|
|
@@ -129,3 +148,17 @@ export async function getAllSettings() {
|
|
|
129
148
|
}
|
|
130
149
|
return settings;
|
|
131
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Closes the config SQLite client to release the file handle on prism-config.db.
|
|
153
|
+
* Called by the lifecycle module during graceful shutdown.
|
|
154
|
+
*/
|
|
155
|
+
export function closeConfigStorage() {
|
|
156
|
+
if (configClient) {
|
|
157
|
+
try {
|
|
158
|
+
configClient.close();
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
console.error(`[ConfigStorage] Error closing db:`, e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|