prism-mcp-server 20.7.1 → 20.8.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 CHANGED
@@ -56,6 +56,30 @@ Prism works locally without an account, API key, or cloud subscription. Add a
56
56
  Synalux subscription when you want cloud memory, paid-tier skills, or team
57
57
  features.
58
58
 
59
+ ### Install as a plugin
60
+
61
+ Prism also ships as a plugin, which registers the MCP server and the startup
62
+ skill for you.
63
+
64
+ **Claude Code** — from the community marketplace:
65
+
66
+ ```bash
67
+ /plugin marketplace add anthropics/claude-plugins-community
68
+ /plugin install synalux-prism@claude-community
69
+ ```
70
+
71
+ **Codex** — this repository is itself a plugin marketplace:
72
+
73
+ ```bash
74
+ codex plugin marketplace add dcostenco/prism-coder
75
+ codex plugin add synalux-prism@prism
76
+ ```
77
+
78
+ The plugin registers `prism-mcp` via `npx -y prism-mcp-server`. If you already
79
+ configured Prism by hand — `prism connect` writes an `mcp_servers.prism-mcp`
80
+ entry — you have that server twice under one key. Install the plugin **or**
81
+ run `prism connect`, not both.
82
+
59
83
  ### What `prism connect` changes about host subagents
60
84
 
61
85
  `connect` steers bounded work to `prism_infer` on your machine rather than to
package/dist/connect.js CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 as win32Path } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1287,6 +1287,29 @@ function registerCodexTomlHost(definition, entry, dryRun, refresh, beforeCommit)
1287
1287
  return result(definition, "error", `could not parse config: ${error instanceof Error ? error.message : String(error)}`);
1288
1288
  }
1289
1289
  }
1290
+ // A Codex plugin may already register prism-mcp. Writing our own entry would
1291
+ // configure the same server twice under one key, so every Prism tool appears
1292
+ // in duplicate and load order decides which wins. Skip instead, and say why —
1293
+ // an unexplained no-op is worse than the duplicate it prevents.
1294
+ // Build the set of plugins Codex will actually load: present in [plugins]
1295
+ // and not explicitly disabled. Detection must not fire on a stale cache
1296
+ // entry for a plugin that is disabled or gone.
1297
+ const enabledPluginKeys = new Set();
1298
+ const pluginsTable = isJsonObject(config) ? config.plugins : undefined;
1299
+ if (isJsonObject(pluginsTable)) {
1300
+ for (const [key, entry] of Object.entries(pluginsTable)) {
1301
+ if (isJsonObject(entry) && entry.enabled !== false)
1302
+ enabledPluginKeys.add(key);
1303
+ }
1304
+ }
1305
+ const providingPlugin = codexPluginProvidesPrismMcp(dirname(configPath), enabledPluginKeys);
1306
+ if (providingPlugin && !locateCodexManagedBlock(originalText ?? "")) {
1307
+ // "existing" rather than a new status: the server IS already registered,
1308
+ // just by the plugin rather than by us. Nothing to do is the truth.
1309
+ return result(definition, "existing", `the Codex plugin ${providingPlugin} already registers prism-mcp; ` +
1310
+ "not adding a second registration under the same key. Remove the plugin " +
1311
+ "if you would rather prism connect manage this server.");
1312
+ }
1290
1313
  let managedBlock;
1291
1314
  try {
1292
1315
  managedBlock = locateCodexManagedBlock(originalText ?? "");
@@ -1377,6 +1400,71 @@ function serializeCodexManagedBlock(entry, existingText) {
1377
1400
  const serialized = stringifyToml({ mcp_servers: { "prism-mcp": entry } }).trimEnd();
1378
1401
  return `${CODEX_MANAGED_START}\n${serialized}\n${CODEX_MANAGED_END}\n`.replaceAll("\n", newline);
1379
1402
  }
1403
+ /**
1404
+ * Is a Codex plugin already providing the prism-mcp server?
1405
+ *
1406
+ * Installing the plugin and running `prism connect` both register a server
1407
+ * under the key `prism-mcp`, so doing both leaves the same server configured
1408
+ * twice — every Prism tool appears in duplicate, and which one wins depends on
1409
+ * load order. Previously this was only documented ("install the plugin OR run
1410
+ * prism connect, not both"), which puts the burden on the user to remember.
1411
+ *
1412
+ * Detection reads the installed plugin's own manifest rather than matching a
1413
+ * plugin NAME, so it keeps working if the plugin is renamed or vendored:
1414
+ * <codexHome>/plugins/cache/<marketplace>/<plugin>/<version>/.mcp.json
1415
+ *
1416
+ * A cache manifest is necessary but NOT sufficient: a disabled or half-removed
1417
+ * plugin leaves its .mcp.json on disk (verified 2026-08-06 — flipping
1418
+ * `enabled = false` in config.toml does not clear the cache). Counting file
1419
+ * presence alone would make `prism connect` skip its own registration for a
1420
+ * plugin that is not actually providing the server, leaving the user with no
1421
+ * prism-mcp at all. So the plugin@marketplace key must ALSO be enabled in the
1422
+ * caller's parsed config for it to count.
1423
+ */
1424
+ export function codexPluginProvidesPrismMcp(codexHome, enabledPluginKeys) {
1425
+ const cacheRoot = join(codexHome, "plugins", "cache");
1426
+ let marketplaces;
1427
+ try {
1428
+ marketplaces = readdirSync(cacheRoot);
1429
+ }
1430
+ catch {
1431
+ return null; // no plugins installed
1432
+ }
1433
+ for (const marketplace of marketplaces) {
1434
+ let plugins;
1435
+ try {
1436
+ plugins = readdirSync(join(cacheRoot, marketplace));
1437
+ }
1438
+ catch {
1439
+ continue;
1440
+ }
1441
+ for (const plugin of plugins) {
1442
+ let versions;
1443
+ try {
1444
+ versions = readdirSync(join(cacheRoot, marketplace, plugin));
1445
+ }
1446
+ catch {
1447
+ continue;
1448
+ }
1449
+ for (const version of versions) {
1450
+ const manifest = join(cacheRoot, marketplace, plugin, version, ".mcp.json");
1451
+ try {
1452
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
1453
+ const key = `${plugin}@${marketplace}`;
1454
+ if (enabledPluginKeys.has(key) &&
1455
+ parsed?.mcpServers &&
1456
+ Object.prototype.hasOwnProperty.call(parsed.mcpServers, "prism-mcp")) {
1457
+ return key;
1458
+ }
1459
+ }
1460
+ catch {
1461
+ // absent or unreadable manifest — not a provider
1462
+ }
1463
+ }
1464
+ }
1465
+ }
1466
+ return null;
1467
+ }
1380
1468
  function validateCodexCandidate(text) {
1381
1469
  const parsed = parseToml(text);
1382
1470
  if (!isJsonObject(parsed) || !isJsonObject(parsed.mcp_servers)) {
@@ -5,12 +5,60 @@ import { getSetting } from "./configStorage.js";
5
5
  export function isValidHttpUrl(url) {
6
6
  try {
7
7
  const parsed = new URL(url);
8
- return parsed.protocol === "http:" || parsed.protocol === "https:";
8
+ if (parsed.protocol === "https:")
9
+ return true;
10
+ // Plain http is accepted ONLY for loopback, where the traffic never leaves
11
+ // the machine — the local Supabase stack runs on 127.0.0.1:54321.
12
+ //
13
+ // Every caller of this function is gating a CLOUD backend, so accepting
14
+ // http for a remote host meant session content — summaries, decisions,
15
+ // filenames — could be sent unencrypted. The privacy policy states this
16
+ // traffic travels over TLS; before this change that was true only because
17
+ // the default base URL happens to be https, not because anything enforced
18
+ // it. A published claim should be guaranteed by the code, not by a default
19
+ // the user can silently override.
20
+ if (parsed.protocol !== "http:")
21
+ return false;
22
+ const host = parsed.hostname;
23
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
9
24
  }
10
25
  catch {
11
26
  return false;
12
27
  }
13
28
  }
29
+ /**
30
+ * Upgrade a remote http:// cloud URL to https:// rather than rejecting it.
31
+ *
32
+ * Rejecting was the first fix for the plaintext gap, and it closed the hole
33
+ * but reported it badly: a user who set an http base URL got "credentials are
34
+ * missing or invalid", which says nothing about the protocol being the
35
+ * problem. Upgrading keeps the guarantee — session content never leaves over
36
+ * plaintext — while letting a working configuration keep working.
37
+ *
38
+ * Loopback is left alone: http on 127.0.0.1 never crosses a network, and the
39
+ * local Supabase stack serves plain http on 54321.
40
+ *
41
+ * The upgrade is announced, not silent. If the host genuinely has no TLS
42
+ * listener the connection fails afterwards, and the log line is what explains
43
+ * why.
44
+ */
45
+ export function upgradeInsecureCloudUrl(raw) {
46
+ try {
47
+ const parsed = new URL(raw);
48
+ if (parsed.protocol !== "http:")
49
+ return raw;
50
+ const host = parsed.hostname;
51
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]")
52
+ return raw;
53
+ parsed.protocol = "https:";
54
+ const upgraded = parsed.toString().replace(/\/+$/, "");
55
+ debugLog(`[Prism Storage] Upgraded ${raw} to ${upgraded}: session content is never sent over plaintext to a remote host.`);
56
+ return upgraded;
57
+ }
58
+ catch {
59
+ return raw;
60
+ }
61
+ }
14
62
  /**
15
63
  * Probe for synalux credentials: env vars first, then config DB.
16
64
  * Returns true if usable credentials are now in process.env.
@@ -21,12 +69,14 @@ export async function ensureSynaluxCredentials() {
21
69
  // Re-check process.env directly: SYNALUX_CONFIGURED is captured at module
22
70
  // load, so credentials injected later by another caller would be invisible
23
71
  // to it. Mirrors ensureSupabaseCredentials below.
24
- const envUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() || process.env.SYNALUX_BASE_URL?.trim();
72
+ const rawEnvUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() || process.env.SYNALUX_BASE_URL?.trim();
73
+ const envUrl = rawEnvUrl ? upgradeInsecureCloudUrl(rawEnvUrl) : rawEnvUrl;
25
74
  const envKey = process.env.PRISM_SYNALUX_API_KEY?.trim();
26
75
  if (envUrl && envKey && isValidHttpUrl(envUrl))
27
76
  return true;
28
- const url = (await getSetting("PRISM_SYNALUX_BASE_URL"))?.trim() ||
77
+ const rawUrl = (await getSetting("PRISM_SYNALUX_BASE_URL"))?.trim() ||
29
78
  (await getSetting("SYNALUX_BASE_URL"))?.trim();
79
+ const url = rawUrl ? upgradeInsecureCloudUrl(rawUrl) : rawUrl;
30
80
  const key = (await getSetting("PRISM_SYNALUX_API_KEY"))?.trim();
31
81
  if (url && key && isValidHttpUrl(url)) {
32
82
  process.env.PRISM_SYNALUX_BASE_URL = url;
@@ -43,11 +93,13 @@ export async function ensureSynaluxCredentials() {
43
93
  async function ensureSupabaseCredentials() {
44
94
  if (SUPABASE_CONFIGURED)
45
95
  return true;
46
- const envUrl = process.env.SUPABASE_URL?.trim();
96
+ const rawEnvUrl = process.env.SUPABASE_URL?.trim();
97
+ const envUrl = rawEnvUrl ? upgradeInsecureCloudUrl(rawEnvUrl) : rawEnvUrl;
47
98
  const envKey = process.env.SUPABASE_KEY?.trim();
48
99
  if (envUrl && envKey && isValidHttpUrl(envUrl))
49
100
  return true;
50
- const url = (await getSetting("SUPABASE_URL"))?.trim();
101
+ const rawUrl = (await getSetting("SUPABASE_URL"))?.trim();
102
+ const url = rawUrl ? upgradeInsecureCloudUrl(rawUrl) : rawUrl;
51
103
  const key = (await getSetting("SUPABASE_KEY"))?.trim();
52
104
  if (url && key && isValidHttpUrl(url)) {
53
105
  process.env.SUPABASE_URL = url;
@@ -1689,6 +1689,64 @@ async function createLocalStartupStorage() {
1689
1689
  await storage.initialize(true);
1690
1690
  return storage;
1691
1691
  }
1692
+ /** Project name for the first-run demo memory. Its own project so it can never
1693
+ * mix with real work, and trivially removable as a unit. */
1694
+ const DEMO_PROJECT = "prism-demo";
1695
+ /**
1696
+ * First-run seed-and-show: write one demo ledger entry, then READ IT BACK
1697
+ * through the storage layer and render from the read-back row.
1698
+ *
1699
+ * The read-back is the point. Rendering the local variable would prove
1700
+ * nothing — a broken storage backend would still print a convincing
1701
+ * "recalled" block. Rendering only what getLedgerEntries returned means the
1702
+ * block the user sees IS evidence the save→recall loop works on their
1703
+ * machine. If either half fails we return null and the greeting simply
1704
+ * omits the demo — a first run must never break on a demo.
1705
+ */
1706
+ export async function seedAndRecallDemoMemory(conversationId) {
1707
+ try {
1708
+ const storage = await getStorage();
1709
+ // Idempotence before insert: the first_bootstrap_at marker is
1710
+ // check-then-set, so two hosts bootstrapping a fresh machine at once BOTH
1711
+ // take the first-run branch (observed setups run several agents
1712
+ // concurrently). Seeding only when no demo row exists narrows that race
1713
+ // from "every concurrent first run inserts" to a near-simultaneous
1714
+ // read-read window, and the loser still renders the winner's row — the
1715
+ // user sees one demo either way.
1716
+ const existing = (await storage.getLedgerEntries({
1717
+ project: `eq.${DEMO_PROJECT}`,
1718
+ user_id: `eq.${PRISM_USER_ID}`,
1719
+ limit: "1",
1720
+ }));
1721
+ if (existing.length === 0) {
1722
+ await storage.saveLedger({
1723
+ project: DEMO_PROJECT,
1724
+ conversation_id: conversationId,
1725
+ user_id: PRISM_USER_ID,
1726
+ summary: "Prism saved this memory during your first session to demonstrate recall.",
1727
+ todos: ["Try it yourself: ask your agent to `session_save_ledger` at the end of this session"],
1728
+ decisions: ["Demo memory — delete anytime with session_forget_memory or from the dashboard"],
1729
+ keywords: ["demo", "first-run"],
1730
+ });
1731
+ }
1732
+ const rows = (await storage.getLedgerEntries({
1733
+ project: `eq.${DEMO_PROJECT}`,
1734
+ user_id: `eq.${PRISM_USER_ID}`,
1735
+ order: "created_at.desc",
1736
+ limit: "1",
1737
+ }));
1738
+ const recalled = rows[0];
1739
+ if (!recalled?.summary)
1740
+ return null;
1741
+ const todo = Array.isArray(recalled.todos) && recalled.todos[0] ? `\n - TODO it carried: ${recalled.todos[0]}` : "";
1742
+ return (`- 🧠 **Watch this — Prism just saved a memory and recalled it from disk:**\n` +
1743
+ ` - "${recalled.summary}"${todo}\n` +
1744
+ ` - This round-trip is what every future session gets: your decisions, TODOs, and changed files, back the moment you return. (Demo lives in the \`${DEMO_PROJECT}\` project — delete it anytime.)`);
1745
+ }
1746
+ catch {
1747
+ return null;
1748
+ }
1749
+ }
1692
1750
  export async function sessionBootstrapHandler(args = {}, options = {}) {
1693
1751
  if (typeof args !== "object" || args === null || Array.isArray(args)) {
1694
1752
  throw new Error("Invalid arguments for session_bootstrap");
@@ -1759,7 +1817,17 @@ export async function sessionBootstrapHandler(args = {}, options = {}) {
1759
1817
  if (isFirstRun) {
1760
1818
  // Action-first instead of absence-first: every line is a capability or
1761
1819
  // a next step. The wizard exists and is well-built; route to it.
1820
+ //
1821
+ // Seed-and-show: a memory product's payoff is structurally deferred to
1822
+ // session 2 — "it remembered" can't be felt until you come back. So the
1823
+ // first run seeds one clearly-marked demo memory and shows it RECALLED,
1824
+ // read back through the real storage layer (not string theater), so the
1825
+ // save→recall loop is felt in session 1. The demo lives in its own
1826
+ // project so it never mixes with real work, and this branch only runs
1827
+ // once — the first_bootstrap_at marker above is durable.
1828
+ const demoRecall = await seedAndRecallDemoMemory(conversationId);
1762
1829
  const firstRunText = `${greeting}\n\n` +
1830
+ (demoRecall ? `${demoRecall}\n` : "") +
1763
1831
  `- 🚀 **Get started:** run the \`onboarding_wizard\` tool (guided setup, ~3 minutes)\n` +
1764
1832
  `${dashboardLine}\n` +
1765
1833
  `- 💾 **Already working?** \`session_save_ledger\` records this session; the next one resumes with full context\n\n` +
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.7.1",
3
+ "version": "20.8.0",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
- "description": "Prism Coder \u2014 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 or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B\u201332B open-weights LLM fleet.",
5
+ "description": "Persistent session memory for AI coding agents that never leaves your machine \u2014 including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
6
6
  "module": "index.ts",
7
7
  "type": "module",
8
8
  "main": "dist/server.js",
@@ -35,68 +35,18 @@
35
35
  "import": "node dist/utils/universalImporter.js"
36
36
  },
37
37
  "keywords": [
38
- "mcp",
39
38
  "mcp-server",
40
- "model-context-protocol",
41
- "ai-agent",
42
39
  "session-memory",
43
40
  "agent-memory",
44
- "mind-palace",
45
41
  "local-first",
46
- "sqlite",
47
- "time-travel",
48
- "visual-memory",
49
- "multi-agent-sync",
50
- "telepathy",
51
- "morning-briefing",
52
- "reality-drift",
53
- "code-mode-templates",
54
- "semantic-search",
55
- "vector-search",
56
- "pgvector",
57
- "f32-blob",
58
- "concurrency-control",
59
- "mcp-prompts",
60
- "mcp-resources",
61
- "google-gemini",
62
- "brave-search",
63
- "llm",
64
- "claude-desktop",
65
- "supabase",
66
- "progressive-context-loading",
67
- "knowledge-accumulation",
68
- "ai-memory",
69
- "ai-tools",
70
- "typescript",
71
- "rag",
72
- "embeddings",
73
- "cursor",
42
+ "local-inference",
43
+ "drift-detection",
44
+ "knowledge-graph",
45
+ "claude-code",
74
46
  "codex",
75
- "windsurf",
76
- "cline",
77
- "persistent-memory",
78
- "zero-config",
79
- "auto-capture",
80
- "dashboard",
81
- "actr",
82
- "cognitive-memory",
83
- "cognitive-architecture",
84
- "activation-memory",
85
- "spreading-activation",
86
- "hebbian-learning",
87
- "multi-hop-reasoning",
88
- "rejection-gate",
89
- "episodic-semantic",
90
- "dark-factory",
91
- "autonomous-pipeline",
92
- "fail-closed",
93
- "anti-sycophancy",
94
- "local-embeddings",
95
- "transformers-js",
96
- "nomic-embed",
97
- "business",
98
- "enterprise",
99
- "hipaa"
47
+ "cursor",
48
+ "offline",
49
+ "rag"
100
50
  ],
101
51
  "homepage": "https://github.com/dcostenco/prism-coder",
102
52
  "repository": {