prism-mcp-server 20.0.7 → 20.0.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.
package/README.md CHANGED
@@ -18,6 +18,16 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
18
18
 
19
19
  ---
20
20
 
21
+ ## What's New in v20.0.8
22
+
23
+ ### verify_behavior Works Again
24
+ The `verify_behavior` tool crashed on every call (`-32602 expected object, received string`) — the handler returned a bare string instead of an MCP `CallToolResult` object. Fixed, with contract + fail-closed regression tests so the safety gate can never silently break again. If you're on 20.0.6/20.0.7, update.
25
+
26
+ ### From v20.0.7: Reserved-Content Safety, Skills Auth, Delegation Metrics
27
+ Reserved clinical content is now Claude-or-refuse (never served by a smaller model than the one that refused it), skill delivery gained a JWT auth fallback (paid-tier skills now reach machines using only `PRISM_SYNALUX_API_KEY`), and every `prism_infer` call is recorded in a persistent `infer_metrics` ledger. Full details in [CHANGELOG.md](CHANGELOG.md).
28
+
29
+ ---
30
+
21
31
  ## What's New in v20.0.5
22
32
 
23
33
  ### Local-First Delegation — 15 Categories, Measured Rate
@@ -23,7 +23,16 @@ const FALLBACK_SCENARIO = [
23
23
  "",
24
24
  "Answer concretely. If you cannot, READ THE FILE FIRST.",
25
25
  ].join("\n");
26
+ /**
27
+ * MCP tool entrypoint. Every tool handler MUST return a CallToolResult
28
+ * object ({ content: [{ type: "text", text }] }) — returning a bare string
29
+ * makes the MCP SDK reject the result ("expected object, received string",
30
+ * -32602). See the dispatch contract in server.ts (result.content usage).
31
+ */
26
32
  export async function verifyBehaviorHandler(args) {
33
+ return { content: [{ type: "text", text: await buildScenarioText(args) }] };
34
+ }
35
+ async function buildScenarioText(args) {
27
36
  if (!SYNALUX_CONFIGURED || !PRISM_SYNALUX_BASE_URL) {
28
37
  return FALLBACK_SCENARIO;
29
38
  }
@@ -9,9 +9,9 @@
9
9
  * stateless MCP), pointed at free-form generation instead of tool-call
10
10
  * responses.
11
11
  *
12
- * Cascade role: prism-coder:4b is the default verifier (fast, 2.5GB).
12
+ * Cascade role: qwen3.5:4b is the default verifier (fast, 2.5GB).
13
13
  * 14b drafts; 4b verifies. Different model = Patronus rule satisfied.
14
- * Falls back to 1b7 on devices with <4GB free RAM.
14
+ * Falls back to 2b on devices with <4GB free RAM.
15
15
  *
16
16
  * Failure modes:
17
17
  * - Verifier model unreachable / timeout → fail-closed refusal
@@ -93,7 +93,7 @@ function refusalText(action, failedClaim) {
93
93
  }
94
94
  }
95
95
  export async function verifyGrounding(opts) {
96
- const verifierModel = opts.verifierModel ?? "prism-coder:4b";
96
+ const verifierModel = opts.verifierModel ?? "qwen3.5:4b";
97
97
  const timeoutMs = opts.timeoutMs ?? 2000;
98
98
  const ollamaUrl = opts.ollamaUrl ?? PRISM_LOCAL_LLM_URL;
99
99
  const fetchImpl = opts.fetchImpl ?? fetch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.0.7",
3
+ "version": "20.0.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",
@@ -1,453 +0,0 @@
1
- /**
2
- * Agent Tools — Local workspace tools for the Prism Agent Terminal
3
- * =================================================================
4
- *
5
- * Provides file system, shell, and memory access tools that the AI agent
6
- * can invoke during an interactive `prism prompt` session. These are
7
- * declared as Gemini Function Declarations and executed locally.
8
- *
9
- * Mirrors the Synalux VS Code extension's local-tools.ts capabilities
10
- * but runs in a terminal context (no VS Code API dependency).
11
- */
12
- import * as fs from "fs";
13
- import * as path from "path";
14
- import { exec } from "child_process";
15
- import { promisify } from "util";
16
- import { SchemaType } from "@google/generative-ai";
17
- import { openUrlCommand, fetchUrlCommand, listFilesCommand, searchFilesCommand, resolveCli, IS_WINDOWS, } from "./platformUtils.js";
18
- const execAsync = promisify(exec);
19
- // ---------------------------------------------------------------------------
20
- // Tool Declarations (Gemini Function Calling Schema)
21
- // ---------------------------------------------------------------------------
22
- export const AGENT_TOOL_DECLARATIONS = [
23
- {
24
- name: "read_file",
25
- description: "Read the contents of a file. Use when the user asks about code, configs, or any file. Supports optional line range.",
26
- parameters: {
27
- type: SchemaType.OBJECT,
28
- properties: {
29
- path: {
30
- type: SchemaType.STRING,
31
- description: "Absolute or relative file path to read",
32
- },
33
- start_line: {
34
- type: SchemaType.INTEGER,
35
- description: "Optional start line (1-indexed)",
36
- },
37
- end_line: {
38
- type: SchemaType.INTEGER,
39
- description: "Optional end line (1-indexed, inclusive)",
40
- },
41
- },
42
- required: ["path"],
43
- },
44
- },
45
- {
46
- name: "list_files",
47
- description: "List files and directories. Use when the user asks about project structure or wants to find files.",
48
- parameters: {
49
- type: SchemaType.OBJECT,
50
- properties: {
51
- directory: {
52
- type: SchemaType.STRING,
53
- description: "Directory path to list (default: current working directory)",
54
- },
55
- pattern: {
56
- type: SchemaType.STRING,
57
- description: "Optional glob pattern filter (e.g., '**/*.ts')",
58
- },
59
- max_depth: {
60
- type: SchemaType.INTEGER,
61
- description: "Maximum directory depth to traverse (default: 3)",
62
- },
63
- },
64
- },
65
- },
66
- {
67
- name: "search_files",
68
- description: "Search for text or patterns across files using ripgrep. Use when the user wants to find where something is defined or used.",
69
- parameters: {
70
- type: SchemaType.OBJECT,
71
- properties: {
72
- query: {
73
- type: SchemaType.STRING,
74
- description: "Text or regex pattern to search for",
75
- },
76
- directory: {
77
- type: SchemaType.STRING,
78
- description: "Directory to search in (default: cwd)",
79
- },
80
- file_pattern: {
81
- type: SchemaType.STRING,
82
- description: "Glob to filter files (e.g., '*.ts')",
83
- },
84
- max_results: {
85
- type: SchemaType.INTEGER,
86
- description: "Maximum results (default: 20)",
87
- },
88
- },
89
- required: ["query"],
90
- },
91
- },
92
- {
93
- name: "run_command",
94
- description: "Execute a shell command and return stdout/stderr. Use for running tests, builds, git commands, etc. Commands have a 30-second timeout.",
95
- parameters: {
96
- type: SchemaType.OBJECT,
97
- properties: {
98
- command: {
99
- type: SchemaType.STRING,
100
- description: "Shell command to execute",
101
- },
102
- cwd: {
103
- type: SchemaType.STRING,
104
- description: "Working directory (default: cwd)",
105
- },
106
- },
107
- required: ["command"],
108
- },
109
- },
110
- {
111
- name: "write_file",
112
- description: "Create or overwrite a file with the given content. Creates parent directories if needed.",
113
- parameters: {
114
- type: SchemaType.OBJECT,
115
- properties: {
116
- path: {
117
- type: SchemaType.STRING,
118
- description: "File path to write to",
119
- },
120
- content: {
121
- type: SchemaType.STRING,
122
- description: "Content to write",
123
- },
124
- },
125
- required: ["path", "content"],
126
- },
127
- },
128
- {
129
- name: "edit_file",
130
- description: "Apply a targeted search-and-replace edit to a file. Use instead of write_file when making small changes.",
131
- parameters: {
132
- type: SchemaType.OBJECT,
133
- properties: {
134
- path: {
135
- type: SchemaType.STRING,
136
- description: "File path to edit",
137
- },
138
- search: {
139
- type: SchemaType.STRING,
140
- description: "Exact text to find and replace",
141
- },
142
- replace: {
143
- type: SchemaType.STRING,
144
- description: "Replacement text",
145
- },
146
- },
147
- required: ["path", "search", "replace"],
148
- },
149
- },
150
- {
151
- name: "memory_search",
152
- description: "Search the user's Prism memory (past sessions, decisions, TODOs). Use when the user asks about previous work or project history.",
153
- parameters: {
154
- type: SchemaType.OBJECT,
155
- properties: {
156
- query: {
157
- type: SchemaType.STRING,
158
- description: "Search query",
159
- },
160
- project: {
161
- type: SchemaType.STRING,
162
- description: "Project to search within",
163
- },
164
- limit: {
165
- type: SchemaType.INTEGER,
166
- description: "Max results (default: 5)",
167
- },
168
- },
169
- required: ["query"],
170
- },
171
- },
172
- {
173
- name: "open_url",
174
- description: "Open a URL in the user's default browser.",
175
- parameters: {
176
- type: SchemaType.OBJECT,
177
- properties: {
178
- url: {
179
- type: SchemaType.STRING,
180
- description: "URL to open",
181
- },
182
- },
183
- required: ["url"],
184
- },
185
- },
186
- {
187
- name: "fetch_url",
188
- description: "Fetch the text content of a web page and return it. Use when the user wants you to read, summarize, or analyze a web page.",
189
- parameters: {
190
- type: SchemaType.OBJECT,
191
- properties: {
192
- url: {
193
- type: SchemaType.STRING,
194
- description: "The URL to fetch content from",
195
- },
196
- },
197
- required: ["url"],
198
- },
199
- },
200
- {
201
- name: "supabase_cli",
202
- description: "Execute Supabase CLI commands (e.g., status, diff, push, db pull). Always use this for Supabase database management.",
203
- parameters: {
204
- type: SchemaType.OBJECT,
205
- properties: {
206
- args: {
207
- type: SchemaType.STRING,
208
- description: 'The arguments to pass to the supabase CLI (e.g., "db pull", "status")',
209
- },
210
- },
211
- required: ["args"],
212
- },
213
- },
214
- {
215
- name: "stripe_cli",
216
- description: "Execute Stripe CLI commands (e.g., listen, trigger, login). Always use this for Stripe operations.",
217
- parameters: {
218
- type: SchemaType.OBJECT,
219
- properties: {
220
- args: {
221
- type: SchemaType.STRING,
222
- description: 'The arguments to pass to the stripe CLI (e.g., "listen --forward-to localhost:3000/api/webhook")',
223
- },
224
- },
225
- required: ["args"],
226
- },
227
- },
228
- ];
229
- // ---------------------------------------------------------------------------
230
- // Tool Executor
231
- // ---------------------------------------------------------------------------
232
- /**
233
- * Resolve a path — if relative, resolve against cwd.
234
- */
235
- function resolvePath(filePath) {
236
- if (path.isAbsolute(filePath))
237
- return filePath;
238
- return path.resolve(process.cwd(), filePath);
239
- }
240
- /**
241
- * Execute an agent tool and return the result as a string.
242
- */
243
- export async function executeAgentTool(toolName, args, project) {
244
- switch (toolName) {
245
- // ─── read_file ─────────────────────────────────────────────
246
- case "read_file": {
247
- const filePath = resolvePath(args.path);
248
- if (!fs.existsSync(filePath))
249
- return `Error: File not found: ${filePath}`;
250
- const content = fs.readFileSync(filePath, "utf-8");
251
- const lines = content.split("\n");
252
- const start = Math.max(1, args.start_line || 1);
253
- const end = Math.min(lines.length, args.end_line || lines.length);
254
- const slice = lines.slice(start - 1, end);
255
- // Add line numbers
256
- const numbered = slice.map((l, i) => `${start + i}: ${l}`).join("\n");
257
- return `File: ${filePath} (${lines.length} lines total, showing ${start}-${end})\n\n${numbered}`;
258
- }
259
- // ─── list_files ────────────────────────────────────────────
260
- case "list_files": {
261
- const dir = resolvePath(args.directory || ".");
262
- if (!fs.existsSync(dir))
263
- return `Error: Directory not found: ${dir}`;
264
- const maxDepth = args.max_depth || 3;
265
- const pattern = args.pattern;
266
- const cmd = listFilesCommand(dir, maxDepth, pattern);
267
- try {
268
- const { stdout } = await execAsync(cmd, { timeout: 10000, shell: IS_WINDOWS ? "powershell.exe" : undefined });
269
- const entries = stdout.trim().split("\n").filter(Boolean);
270
- return `Directory: ${dir}\n${entries.length} items found:\n\n${entries.join("\n")}`;
271
- }
272
- catch {
273
- // Fallback to Node.js readdir
274
- const entries = fs.readdirSync(dir, { withFileTypes: true });
275
- const lines = entries
276
- .slice(0, 50)
277
- .map((e) => `${e.isDirectory() ? "📁" : "📄"} ${e.name}`);
278
- return `Directory: ${dir}\n${lines.join("\n")}`;
279
- }
280
- }
281
- // ─── search_files ──────────────────────────────────────────
282
- case "search_files": {
283
- const query = args.query;
284
- const dir = resolvePath(args.directory || ".");
285
- const maxResults = args.max_results || 20;
286
- const filePattern = args.file_pattern;
287
- const cmd = searchFilesCommand(query, dir, maxResults, filePattern);
288
- try {
289
- const { stdout } = await execAsync(cmd, { timeout: 15000 });
290
- if (!stdout.trim())
291
- return `No matches found for "${query}" in ${dir}`;
292
- return `Search: "${query}" in ${dir}\n\n${stdout.trim()}`;
293
- }
294
- catch {
295
- return `No matches found for "${query}" in ${dir}`;
296
- }
297
- }
298
- // ─── run_command ───────────────────────────────────────────
299
- case "run_command": {
300
- const command = args.command;
301
- const cwd = resolvePath(args.cwd || ".");
302
- // Safety: block obviously dangerous commands
303
- const blocked = ["rm -rf /", "mkfs", "dd if=", ":(){"];
304
- if (blocked.some((b) => command.includes(b))) {
305
- return "Error: Command blocked for safety reasons.";
306
- }
307
- try {
308
- const { stdout, stderr } = await execAsync(command, {
309
- cwd,
310
- timeout: 30000,
311
- maxBuffer: 1024 * 1024,
312
- env: { ...process.env, PAGER: "cat" },
313
- });
314
- let result = "";
315
- if (stdout.trim())
316
- result += `stdout:\n${stdout.trim()}\n`;
317
- if (stderr.trim())
318
- result += `stderr:\n${stderr.trim()}\n`;
319
- if (!result)
320
- result = "(command completed with no output)";
321
- return result;
322
- }
323
- catch (err) {
324
- const e = err;
325
- let msg = `Command failed: ${command}\n`;
326
- if (e.stdout)
327
- msg += `stdout:\n${e.stdout}\n`;
328
- if (e.stderr)
329
- msg += `stderr:\n${e.stderr}\n`;
330
- if (e.message && !e.stderr)
331
- msg += `error: ${e.message}`;
332
- return msg;
333
- }
334
- }
335
- // ─── write_file ────────────────────────────────────────────
336
- case "write_file": {
337
- const filePath = resolvePath(args.path);
338
- const content = args.content;
339
- // Create parent dirs
340
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
341
- fs.writeFileSync(filePath, content, "utf-8");
342
- return `✅ Written ${content.length} chars to ${filePath}`;
343
- }
344
- // ─── edit_file ─────────────────────────────────────────────
345
- case "edit_file": {
346
- const filePath = resolvePath(args.path);
347
- if (!fs.existsSync(filePath))
348
- return `Error: File not found: ${filePath}`;
349
- const original = fs.readFileSync(filePath, "utf-8");
350
- const search = args.search;
351
- const replace = args.replace;
352
- if (!original.includes(search)) {
353
- return `Error: Search text not found in ${filePath}`;
354
- }
355
- const updated = original.replace(search, replace);
356
- fs.writeFileSync(filePath, updated, "utf-8");
357
- return `✅ Edited ${filePath} — replaced ${search.length} chars`;
358
- }
359
- // ─── memory_search ─────────────────────────────────────────
360
- case "memory_search": {
361
- const { knowledgeSearchHandler } = await import("../tools/graphHandlers.js");
362
- const result = await knowledgeSearchHandler({
363
- query: args.query,
364
- project: args.project || project || "prism-mcp",
365
- limit: args.limit || 5,
366
- });
367
- const text = result.content?.[0] && "text" in result.content[0]
368
- ? result.content[0].text
369
- : "No results found.";
370
- return text;
371
- }
372
- // ─── open_url ──────────────────────────────────────────────
373
- case "open_url": {
374
- const url = args.url;
375
- try {
376
- await execAsync(openUrlCommand(url));
377
- return `✅ Opened ${url} in default browser`;
378
- }
379
- catch {
380
- return `Error: Failed to open ${url}`;
381
- }
382
- }
383
- // ─── fetch_url ─────────────────────────────────────────────
384
- case "fetch_url": {
385
- const fetchUrl = args.url;
386
- if (!fetchUrl?.match(/^https?:\/\//i)) {
387
- return "Error: Invalid URL. Must start with http:// or https://";
388
- }
389
- try {
390
- const cmd = fetchUrlCommand(fetchUrl);
391
- const { stdout } = await execAsync(cmd, {
392
- timeout: 20000,
393
- maxBuffer: 1024 * 1024,
394
- shell: IS_WINDOWS ? "powershell.exe" : undefined,
395
- });
396
- const text = stdout.trim().slice(0, 8000);
397
- return `Content from ${fetchUrl}:\n\n${text}${text.length >= 8000 ? "\n\n... (truncated)" : ""}`;
398
- }
399
- catch (err) {
400
- const e = err;
401
- return `Error: Failed to fetch ${fetchUrl} — ${e.message?.slice(0, 200) || "unknown error"}`;
402
- }
403
- }
404
- // ─── supabase_cli ──────────────────────────────────────────
405
- case "supabase_cli": {
406
- const sbArgs = args.args;
407
- const sbBin = resolveCli("supabase");
408
- try {
409
- const { stdout } = await execAsync(`${sbBin} ${sbArgs}`, {
410
- cwd: process.cwd(),
411
- timeout: 60000,
412
- maxBuffer: 1024 * 512,
413
- env: { ...process.env, PAGER: "cat" },
414
- });
415
- return `supabase ${sbArgs}:\n${stdout.trim().slice(0, 5000)}`;
416
- }
417
- catch (err) {
418
- const e = err;
419
- let msg = `supabase ${sbArgs} failed:\n`;
420
- if (e.stdout)
421
- msg += e.stdout.slice(0, 3000);
422
- if (e.stderr)
423
- msg += `\nstderr: ${e.stderr.slice(0, 1000)}`;
424
- return msg;
425
- }
426
- }
427
- // ─── stripe_cli ───────────────────────────────────────────
428
- case "stripe_cli": {
429
- const stripeArgs = args.args;
430
- const stripeBin = resolveCli("stripe");
431
- try {
432
- const { stdout } = await execAsync(`${stripeBin} ${stripeArgs}`, {
433
- cwd: process.cwd(),
434
- timeout: 60000,
435
- maxBuffer: 1024 * 512,
436
- env: { ...process.env, PAGER: "cat" },
437
- });
438
- return `stripe ${stripeArgs}:\n${stdout.trim().slice(0, 5000)}`;
439
- }
440
- catch (err) {
441
- const e = err;
442
- let msg = `stripe ${stripeArgs} failed:\n`;
443
- if (e.stdout)
444
- msg += e.stdout.slice(0, 3000);
445
- if (e.stderr)
446
- msg += `\nstderr: ${e.stderr.slice(0, 1000)}`;
447
- return msg;
448
- }
449
- }
450
- default:
451
- return `Error: Unknown tool \"${toolName}\"`;
452
- }
453
- }