prism-mcp-server 19.2.2 → 19.2.5
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 +40 -4
- package/dist/server.js +9 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/ledgerHandlers.js +94 -9
- package/dist/tools/sessionMemoryDefinitions.js +12 -0
- package/dist/utils/inferenceMetrics.js +10 -1
- package/dist/utils/notifier.js +20 -4
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Every `prism_infer` call tracks which model handled it (local Ollama vs cloud) a
|
|
|
93
93
|
synalux-27b: 2 calls, 1,500 tokens, avg 1,100ms
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
Local calls use actual Ollama token counts; cloud calls use estimates. Metrics are
|
|
96
|
+
Local calls use actual Ollama token counts (`prompt_eval_count` / `eval_count` from Ollama); cloud calls use char/4 estimates. Metrics are tracked locally — no portal dependency, no env vars, works offline. Per-call data is also forwarded to the Synalux portal as best-effort analytics (independent of the display).
|
|
97
97
|
|
|
98
98
|
### Session Drift Detection
|
|
99
99
|
|
|
@@ -363,7 +363,7 @@ All on-device models are free to run locally via Ollama on every tier. A subscri
|
|
|
363
363
|
| Cloud Coder (Web IDE) | -- | 100/day | 1,000/day | 100,000/day |
|
|
364
364
|
| Cloud search | -- | 50/day | 500/day | 100,000/day |
|
|
365
365
|
| Max output tokens | 512 | 1,024 | 2,048 | 4,096 |
|
|
366
|
-
| Cloud fallback | -- | Claude
|
|
366
|
+
| Cloud fallback | -- | Claude Opus 4.7 | Claude Opus 4.7 | Priority + Opus 4.7 |
|
|
367
367
|
| Grounding verifier (fact-check AI output) | -- | ✅ | ✅ | ✅ |
|
|
368
368
|
| Memory sync (cloud) | -- | ✅ | ✅ | ✅ |
|
|
369
369
|
| Knowledge / session memory | limited | unlimited | unlimited | unlimited |
|
|
@@ -389,6 +389,7 @@ Prism exposes 40+ MCP tools. The core memory loop:
|
|
|
389
389
|
| `verify_behavior` | Pre-edit scenario challenge — catch bad changes before they happen |
|
|
390
390
|
| `knowledge_ingest` | Teach Prism a codebase or document |
|
|
391
391
|
| `prism_infer` | Local-first inference (route/chat/code modes, thinking, cloud escalation) |
|
|
392
|
+
| `inference_metrics` | Session delegation stats on demand (call count, tokens, local/cloud split) |
|
|
392
393
|
|
|
393
394
|
### `prism_infer` — local-first inference with cloud escalation
|
|
394
395
|
|
|
@@ -411,6 +412,40 @@ prism_infer({
|
|
|
411
412
|
|
|
412
413
|
Full TypeScript signatures live in [`src/tools/`](src/tools/); architecture in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
413
414
|
|
|
415
|
+
### `inference_metrics` — see your local-model usage on demand
|
|
416
|
+
|
|
417
|
+
Call `inference_metrics` anytime mid-session to see how many `prism_infer` calls ran locally vs cloud, with actual token counts:
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
📊 Inference Metrics — local-model delegation (this session):
|
|
421
|
+
Total calls: 5 — Local: 5 (100%) | Cloud: 0 (0%)
|
|
422
|
+
Tokens: 1,240 in + 380 out = 1,620 total
|
|
423
|
+
Avg latency: 420ms
|
|
424
|
+
By model:
|
|
425
|
+
prism-coder:27b: 3 calls, 1,100 tokens, avg 520ms
|
|
426
|
+
prism-coder:9b: 2 calls, 520 tokens, avg 270ms
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The same block also appears automatically in `session_save_ledger` and `session_save_handoff` responses at session end.
|
|
430
|
+
|
|
431
|
+
**Note:** This tracks `prism_infer` delegation only — not your host model's (Claude's) own token spend. For that, use Claude Code's `/cost` command.
|
|
432
|
+
|
|
433
|
+
### Local-model delegation (opt-in)
|
|
434
|
+
|
|
435
|
+
By default, your AI agent (Claude, Cursor, etc.) handles everything itself. You can optionally enable delegation so the agent offloads cheap, verifiable sub-tasks to local Ollama models at $0:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
# Enable via Prism config
|
|
439
|
+
prism config set delegation_enabled true
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
When enabled, the agent's task router may delegate qualifying work — bulk classification, field extraction, mechanical formatting — to `prism_infer` instead of using cloud tokens. The agent always verifies the result and redoes it itself if quality is degraded.
|
|
443
|
+
|
|
444
|
+
**Guardrails:**
|
|
445
|
+
- **Off by default** — enforced in code, not just convention
|
|
446
|
+
- **Never delegates:** code/text that ships to the user, security/safety logic, planning/reasoning, anything where a silent quality drop isn't obvious
|
|
447
|
+
- **Always verifies:** checks `quality_gate_failed` and `used_cloud` before trusting local output
|
|
448
|
+
|
|
414
449
|
<details>
|
|
415
450
|
<summary>How Prism survives context compaction</summary>
|
|
416
451
|
|
|
@@ -553,6 +588,7 @@ Routing is automatic: `9b → 4b → cloud fallback` on desktop/server, `2b →
|
|
|
553
588
|
| `PRISM_SYNALUX_API_KEY` | Paid-tier portal key (`synalux_sk_...`) | -- (local if unset) |
|
|
554
589
|
| `LOCAL_LLM_URL` | Ollama endpoint | `http://localhost:11434` |
|
|
555
590
|
| `PRISM_FORCE_LOCAL` | Force local SQLite regardless of credentials | `false` |
|
|
591
|
+
| `TELEMETRY_WRITE_TOKEN` | Portal analytics token (optional — metrics display works without it) | -- |
|
|
556
592
|
|
|
557
593
|
With no variables set, Prism runs fully local. Set `PRISM_SYNALUX_API_KEY` (and leave `PRISM_STORAGE=auto`) to use the cloud backend.
|
|
558
594
|
|
|
@@ -561,11 +597,11 @@ With no variables set, Prism runs fully local. Set `PRISM_SYNALUX_API_KEY` (and
|
|
|
561
597
|
## Testing
|
|
562
598
|
|
|
563
599
|
```bash
|
|
564
|
-
npm test # full suite (vitest)
|
|
600
|
+
npm test # full suite (vitest) — 95 files, 2841 tests
|
|
565
601
|
npm test -- --coverage # coverage report
|
|
566
602
|
```
|
|
567
603
|
|
|
568
|
-
Coverage spans HRR retrieval, knowledge ingestion, the inference cascade and grounding verifier, compaction, the model picker, and storage round-trips.
|
|
604
|
+
Coverage spans HRR retrieval, knowledge ingestion, the inference cascade and grounding verifier, inference metrics, telemetry allowlist, delegation gate, compaction, the model picker, and storage round-trips.
|
|
569
605
|
|
|
570
606
|
---
|
|
571
607
|
|
package/dist/server.js
CHANGED
|
@@ -79,6 +79,7 @@ import { sanitizeMcpOutput } from "./utils/sanitizer.js";
|
|
|
79
79
|
import { getTracer, initTelemetry } from "./utils/telemetry.js";
|
|
80
80
|
import { context as otelContext, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
81
81
|
import { ddInfo, ddError as ddLogError } from "./utils/ddLogger.js";
|
|
82
|
+
import { inferenceMetricsHandler } from "./utils/inferenceMetrics.js";
|
|
82
83
|
// ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
|
|
83
84
|
import { WEB_SEARCH_TOOL, BRAVE_WEB_SEARCH_CODE_MODE_TOOL, LOCAL_SEARCH_TOOL, BRAVE_LOCAL_SEARCH_CODE_MODE_TOOL, CODE_MODE_TRANSFORM_TOOL, BRAVE_ANSWERS_TOOL, RESEARCH_PAPER_ANALYSIS_TOOL, webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, braveLocalSearchCodeModeHandler, codeModeTransformHandler, braveAnswersHandler, researchPaperAnalysisHandler, } from "./tools/index.js";
|
|
84
85
|
// Session memory tools — only used if Supabase is configured
|
|
@@ -112,7 +113,9 @@ VERIFY_BEHAVIOR_TOOL, isVerifyBehaviorArgs,
|
|
|
112
113
|
// v12: Developer Onboarding & Enterprise Observability
|
|
113
114
|
ONBOARDING_WIZARD_TOOL, EXTRACT_ENTITIES_TOOL, API_ANALYTICS_TOOL, BACKUP_DATABASE_TOOL, CONFIGURE_NOTIFICATIONS_TOOL, QUERY_MEMORY_NATURAL_TOOL,
|
|
114
115
|
// v15.5: Knowledge Ingestion
|
|
115
|
-
KNOWLEDGE_INGEST_TOOL,
|
|
116
|
+
KNOWLEDGE_INGEST_TOOL,
|
|
117
|
+
// v19.2: Inference Metrics
|
|
118
|
+
INFERENCE_METRICS_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
|
|
116
119
|
// ─── v0.4.0: New tool handlers ───
|
|
117
120
|
compactLedgerHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, sessionBackfillLinksHandler, sessionSynthesizeEdgesHandler, sessionCognitiveRouteHandler,
|
|
118
121
|
// ─── v2.0: Time Travel handlers ───
|
|
@@ -246,6 +249,8 @@ function buildSessionMemoryTools(autoloadList) {
|
|
|
246
249
|
QUERY_MEMORY_NATURAL_TOOL, // query_memory_natural — NL → structured memory search
|
|
247
250
|
// ─── v15.5: Knowledge Ingestion ───
|
|
248
251
|
KNOWLEDGE_INGEST_TOOL, // knowledge_ingest — chunk code, gen Q&A, store in graph
|
|
252
|
+
// ─── v19.2: Inference Metrics ───
|
|
253
|
+
INFERENCE_METRICS_TOOL, // inference_metrics — read-only session delegation stats
|
|
249
254
|
];
|
|
250
255
|
}
|
|
251
256
|
// ─── v0.4.0: Resource Subscription Tracking ──────────────────────
|
|
@@ -960,6 +965,9 @@ export function createServer() {
|
|
|
960
965
|
throw new Error("Session memory not configured.");
|
|
961
966
|
result = await knowledgeIngestHandler(args);
|
|
962
967
|
break;
|
|
968
|
+
case "inference_metrics":
|
|
969
|
+
result = await inferenceMetricsHandler();
|
|
970
|
+
break;
|
|
963
971
|
default:
|
|
964
972
|
result = {
|
|
965
973
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
package/dist/tools/index.js
CHANGED
|
@@ -63,6 +63,7 @@ export { verifyBehaviorHandler } from "./behavioralVerifierHandler.js";
|
|
|
63
63
|
// Chunks source code, generates Q&A via Claude Haiku, stores in knowledge graph.
|
|
64
64
|
// Three entry points: MCP tool, REST API, GitHub webhook.
|
|
65
65
|
export { KNOWLEDGE_INGEST_TOOL } from "./ingestDefinitions.js";
|
|
66
|
+
export { INFERENCE_METRICS_TOOL } from "./sessionMemoryDefinitions.js";
|
|
66
67
|
export { knowledgeIngestHandler, handleGitHubWebhook, ingestKnowledge, isIngestArgs } from "./ingestHandler.js";
|
|
67
68
|
// ── v15.4: prism_infer — local-first inference (RAM-gated cascade) ──
|
|
68
69
|
// Always available. Saves caller's cloud tokens by routing to local
|
|
@@ -40,8 +40,8 @@ isSessionForgetMemoryArgs, // v3.1: TTL retention policy type guard
|
|
|
40
40
|
isSessionSaveExperienceArgs, isSessionExportMemoryArgs, normalizeExportFormat, isSessionSaveImageArgs, isSessionViewImageArgs } from "./sessionMemoryDefinitions.js";
|
|
41
41
|
// v4.2: File system access for knowledge_sync_rules
|
|
42
42
|
import { writeFile } from "node:fs/promises";
|
|
43
|
-
import { existsSync } from "node:fs";
|
|
44
|
-
import { join } from "node:path";
|
|
43
|
+
import { existsSync, realpathSync, mkdirSync } from "node:fs";
|
|
44
|
+
import { join, sep } from "node:path";
|
|
45
45
|
// v3.1: In-memory debounce lock for auto-compaction.
|
|
46
46
|
// Prevents multiple concurrent Gemini compaction tasks for the same project
|
|
47
47
|
// when many agents call session_save_ledger at the same time.
|
|
@@ -1448,7 +1448,13 @@ export async function sessionExportMemoryHandler(args) {
|
|
|
1448
1448
|
// to select the correct .obsidian/.logseq sidecar config.
|
|
1449
1449
|
const requestedFormat = rawFormat;
|
|
1450
1450
|
const format = normalizeExportFormat(rawFormat);
|
|
1451
|
-
// Validate output directory
|
|
1451
|
+
// Validate output directory — path traversal confinement (v4.3 security)
|
|
1452
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
1453
|
+
const defaultExportDir = join(homeDir, ".prism-mcp", "exports");
|
|
1454
|
+
// Create default export directory if it doesn't exist
|
|
1455
|
+
if (!existsSync(defaultExportDir)) {
|
|
1456
|
+
mkdirSync(defaultExportDir, { recursive: true });
|
|
1457
|
+
}
|
|
1452
1458
|
if (!existsSync(output_dir)) {
|
|
1453
1459
|
return {
|
|
1454
1460
|
content: [{
|
|
@@ -1458,6 +1464,72 @@ export async function sessionExportMemoryHandler(args) {
|
|
|
1458
1464
|
isError: true,
|
|
1459
1465
|
};
|
|
1460
1466
|
}
|
|
1467
|
+
// Resolve symlinks and ".." to get the canonical path
|
|
1468
|
+
const resolvedDir = realpathSync(output_dir);
|
|
1469
|
+
// Build allow-list of safe export roots
|
|
1470
|
+
const allowedRoots = [
|
|
1471
|
+
defaultExportDir,
|
|
1472
|
+
process.cwd(),
|
|
1473
|
+
];
|
|
1474
|
+
// Scratch root under tmp — but NOT the world-writable tmp root itself.
|
|
1475
|
+
// os.tmpdir() (typically /tmp, mode 1777) is readable and writable by every
|
|
1476
|
+
// local principal: allowing it as an export root would (a) leak unredacted
|
|
1477
|
+
// ledger/handoff contents to other users and (b) let a co-resident attacker
|
|
1478
|
+
// pre-plant a symlink at the predictable export filename and have writeFile
|
|
1479
|
+
// follow it (arbitrary-file overwrite). Confine to an owner-only subdir.
|
|
1480
|
+
const tmpExportDir = join(os.tmpdir(), ".prism-mcp", "exports");
|
|
1481
|
+
if (!existsSync(tmpExportDir)) {
|
|
1482
|
+
mkdirSync(tmpExportDir, { recursive: true, mode: 0o700 });
|
|
1483
|
+
}
|
|
1484
|
+
allowedRoots.push(realpathSync(tmpExportDir));
|
|
1485
|
+
if (process.env.PRISM_EXPORT_ROOT) {
|
|
1486
|
+
const exportRoot = process.env.PRISM_EXPORT_ROOT;
|
|
1487
|
+
if (existsSync(exportRoot)) {
|
|
1488
|
+
allowedRoots.push(realpathSync(exportRoot));
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
// Reject sensitive directories (resolve each so macOS /etc → /private/etc matches)
|
|
1492
|
+
const sensitiveRoots = [
|
|
1493
|
+
join(homeDir, ".ssh"),
|
|
1494
|
+
join(homeDir, ".gnupg"),
|
|
1495
|
+
"/etc",
|
|
1496
|
+
"/var",
|
|
1497
|
+
"/etc/cron.d",
|
|
1498
|
+
"/etc/cron.daily",
|
|
1499
|
+
"/etc/sudoers.d",
|
|
1500
|
+
].map(p => existsSync(p) ? realpathSync(p) : p);
|
|
1501
|
+
// Check allow-list first — explicitly configured roots take precedence
|
|
1502
|
+
const isAllowedRoot = (dir) => allowedRoots.some((root) => {
|
|
1503
|
+
const resolvedRoot = existsSync(root) ? realpathSync(root) : root;
|
|
1504
|
+
return dir === resolvedRoot || dir.startsWith(resolvedRoot + sep);
|
|
1505
|
+
});
|
|
1506
|
+
for (const sensitive of sensitiveRoots) {
|
|
1507
|
+
if (resolvedDir === sensitive || resolvedDir.startsWith(sensitive + sep)) {
|
|
1508
|
+
// Allow explicitly configured roots even under sensitive parents
|
|
1509
|
+
// (e.g. tmpExportDir under /private/var on macOS, or PRISM_EXPORT_ROOT)
|
|
1510
|
+
if (!isAllowedRoot(resolvedDir)) {
|
|
1511
|
+
return {
|
|
1512
|
+
content: [{
|
|
1513
|
+
type: "text",
|
|
1514
|
+
text: `Error: output_dir resolves to a sensitive system directory ("${resolvedDir}"). Export denied.`,
|
|
1515
|
+
}],
|
|
1516
|
+
isError: true,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Verify resolved path falls under an allowed root
|
|
1522
|
+
if (!isAllowedRoot(resolvedDir)) {
|
|
1523
|
+
return {
|
|
1524
|
+
content: [{
|
|
1525
|
+
type: "text",
|
|
1526
|
+
text: `Error: output_dir "${resolvedDir}" is outside allowed export roots. ` +
|
|
1527
|
+
`Allowed: ${allowedRoots.join(", ")}. ` +
|
|
1528
|
+
`Set PRISM_EXPORT_ROOT env var to add a custom root.`,
|
|
1529
|
+
}],
|
|
1530
|
+
isError: true,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1461
1533
|
const storage = await getStorage();
|
|
1462
1534
|
const exportedFiles = [];
|
|
1463
1535
|
try {
|
|
@@ -1527,14 +1599,27 @@ export async function sessionExportMemoryHandler(args) {
|
|
|
1527
1599
|
else {
|
|
1528
1600
|
content = JSON.stringify(exportPayload, null, 2);
|
|
1529
1601
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1602
|
+
// Exclusive create (flag: "wx") refuses to write if the path already
|
|
1603
|
+
// exists — including a symlink pre-planted at the predictable export
|
|
1604
|
+
// name. On a genuine collision fall back to a timestamped unique name.
|
|
1605
|
+
let finalPath = outputPath;
|
|
1606
|
+
try {
|
|
1607
|
+
await writeFile(finalPath, content, { flag: "wx" });
|
|
1532
1608
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1609
|
+
catch (e) {
|
|
1610
|
+
if (e && e.code === "EEXIST") {
|
|
1611
|
+
const dot = filename.lastIndexOf(".");
|
|
1612
|
+
const stem = dot > 0 ? filename.slice(0, dot) : filename;
|
|
1613
|
+
const extPart = dot > 0 ? filename.slice(dot) : "";
|
|
1614
|
+
finalPath = join(output_dir, `${stem}-${Date.now()}${extPart}`);
|
|
1615
|
+
await writeFile(finalPath, content, { flag: "wx" });
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
throw e;
|
|
1619
|
+
}
|
|
1535
1620
|
}
|
|
1536
|
-
exportedFiles.push(
|
|
1537
|
-
debugLog(`[session_export_memory] Wrote ${content.length} bytes to ${
|
|
1621
|
+
exportedFiles.push(finalPath);
|
|
1622
|
+
debugLog(`[session_export_memory] Wrote ${content.length} bytes to ${finalPath}`);
|
|
1538
1623
|
}
|
|
1539
1624
|
const plural = exportedFiles.length > 1 ? "files" : "file";
|
|
1540
1625
|
return {
|
|
@@ -1790,3 +1790,15 @@ export function isVerifyBehaviorArgs(a) {
|
|
|
1790
1790
|
return false;
|
|
1791
1791
|
return true;
|
|
1792
1792
|
}
|
|
1793
|
+
// ─── v19.2: Inference Metrics Tool ──────────────────────────
|
|
1794
|
+
export const INFERENCE_METRICS_TOOL = {
|
|
1795
|
+
name: "inference_metrics",
|
|
1796
|
+
description: "Returns the current session's local-model inference metrics — call count, " +
|
|
1797
|
+
"local vs cloud split, token totals, per-model breakdown, and average latency. " +
|
|
1798
|
+
"Read-only, no arguments. Reflects prism_infer delegation usage only, not the " +
|
|
1799
|
+
"host model's (Claude's) own token spend (use /cost for that).",
|
|
1800
|
+
inputSchema: {
|
|
1801
|
+
type: "object",
|
|
1802
|
+
properties: {},
|
|
1803
|
+
},
|
|
1804
|
+
};
|
|
@@ -70,12 +70,21 @@ export function resetInferenceMetrics() {
|
|
|
70
70
|
}
|
|
71
71
|
debugLog("[inference-metrics] Session metrics reset");
|
|
72
72
|
}
|
|
73
|
+
export async function inferenceMetricsHandler() {
|
|
74
|
+
const block = formatInferenceMetrics();
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: block || "No prism_infer calls this session. Metrics track local-model delegation only — not the host model's (Claude's) token spend.",
|
|
79
|
+
}],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
73
82
|
export function formatInferenceMetrics() {
|
|
74
83
|
const snap = getInferenceSnapshot();
|
|
75
84
|
if (snap.totalCalls === 0)
|
|
76
85
|
return "";
|
|
77
86
|
const lines = [
|
|
78
|
-
`\n📊 Inference Metrics (this session):`,
|
|
87
|
+
`\n📊 Inference Metrics — local-model delegation (this session):`,
|
|
79
88
|
` Total calls: ${snap.totalCalls} — Local: ${snap.localCalls} (${snap.localPct}%) | Cloud: ${snap.cloudCalls} (${snap.cloudPct}%)`,
|
|
80
89
|
` Tokens: ${snap.totalPromptTokens.toLocaleString()} in + ${snap.totalCompletionTokens.toLocaleString()} out = ${snap.totalTokens.toLocaleString()} total`,
|
|
81
90
|
` Avg latency: ${snap.avgLatencyMs}ms`,
|
package/dist/utils/notifier.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Zero external dependencies — uses native fetch API.
|
|
12
12
|
*/
|
|
13
13
|
import { debugLog } from "./logger.js";
|
|
14
|
+
import { lookup } from "node:dns/promises";
|
|
14
15
|
// ─── Default Config ──────────────────────────────────────────
|
|
15
16
|
const DEFAULT_CONFIG = {
|
|
16
17
|
enabled: false,
|
|
@@ -99,7 +100,7 @@ function isPrivateIP(ip) {
|
|
|
99
100
|
return true;
|
|
100
101
|
return false;
|
|
101
102
|
}
|
|
102
|
-
function isAllowedUrl(url) {
|
|
103
|
+
async function isAllowedUrl(url) {
|
|
103
104
|
try {
|
|
104
105
|
const parsed = new URL(url);
|
|
105
106
|
// Block non-HTTP(S) schemes
|
|
@@ -118,6 +119,18 @@ function isAllowedUrl(url) {
|
|
|
118
119
|
// Block bracketed IPv6
|
|
119
120
|
if (hostname.startsWith("[") && isPrivateIP(hostname))
|
|
120
121
|
return false;
|
|
122
|
+
// Resolve hostname and reject if ANY address is private (closes
|
|
123
|
+
// attacker.example → 169.254.169.254 / 127.0.0.1 bypass)
|
|
124
|
+
try {
|
|
125
|
+
const addrs = await lookup(hostname, { all: true });
|
|
126
|
+
for (const { address } of addrs) {
|
|
127
|
+
if (isPrivateIP(address))
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
121
134
|
return true;
|
|
122
135
|
}
|
|
123
136
|
catch {
|
|
@@ -126,7 +139,7 @@ function isAllowedUrl(url) {
|
|
|
126
139
|
}
|
|
127
140
|
// ─── Channel Senders ─────────────────────────────────────────
|
|
128
141
|
async function sendWebhook(url, payload) {
|
|
129
|
-
if (!isAllowedUrl(url)) {
|
|
142
|
+
if (!(await isAllowedUrl(url))) {
|
|
130
143
|
debugLog(`Webhook URL blocked by SSRF policy: ${url}`);
|
|
131
144
|
return false;
|
|
132
145
|
}
|
|
@@ -135,6 +148,7 @@ async function sendWebhook(url, payload) {
|
|
|
135
148
|
method: "POST",
|
|
136
149
|
headers: { "Content-Type": "application/json" },
|
|
137
150
|
body: JSON.stringify(payload),
|
|
151
|
+
redirect: "error",
|
|
138
152
|
signal: AbortSignal.timeout(10_000),
|
|
139
153
|
});
|
|
140
154
|
return response.ok;
|
|
@@ -145,7 +159,7 @@ async function sendWebhook(url, payload) {
|
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
async function sendSlack(webhookUrl, payload) {
|
|
148
|
-
if (!isAllowedUrl(webhookUrl)) {
|
|
162
|
+
if (!(await isAllowedUrl(webhookUrl))) {
|
|
149
163
|
debugLog(`Slack webhook URL blocked by SSRF policy: ${webhookUrl}`);
|
|
150
164
|
return false;
|
|
151
165
|
}
|
|
@@ -191,6 +205,7 @@ async function sendSlack(webhookUrl, payload) {
|
|
|
191
205
|
method: "POST",
|
|
192
206
|
headers: { "Content-Type": "application/json" },
|
|
193
207
|
body: JSON.stringify(slackPayload),
|
|
208
|
+
redirect: "error",
|
|
194
209
|
signal: AbortSignal.timeout(10_000),
|
|
195
210
|
});
|
|
196
211
|
return response.ok;
|
|
@@ -201,7 +216,7 @@ async function sendSlack(webhookUrl, payload) {
|
|
|
201
216
|
}
|
|
202
217
|
}
|
|
203
218
|
async function sendEmail(endpoint, payload) {
|
|
204
|
-
if (!isAllowedUrl(endpoint)) {
|
|
219
|
+
if (!(await isAllowedUrl(endpoint))) {
|
|
205
220
|
debugLog(`Email endpoint URL blocked by SSRF policy: ${endpoint}`);
|
|
206
221
|
return false;
|
|
207
222
|
}
|
|
@@ -216,6 +231,7 @@ async function sendEmail(endpoint, payload) {
|
|
|
216
231
|
project: payload.project,
|
|
217
232
|
details: payload.details,
|
|
218
233
|
}),
|
|
234
|
+
redirect: "error",
|
|
219
235
|
signal: AbortSignal.timeout(10_000),
|
|
220
236
|
});
|
|
221
237
|
return response.ok;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "19.2.
|
|
3
|
+
"version": "19.2.5",
|
|
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",
|
|
@@ -129,9 +129,10 @@
|
|
|
129
129
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
130
130
|
"@mozilla/readability": "^0.6.0",
|
|
131
131
|
"@opentelemetry/api": "^1.9.1",
|
|
132
|
-
"@opentelemetry/
|
|
133
|
-
"@opentelemetry/
|
|
134
|
-
"@opentelemetry/
|
|
132
|
+
"@opentelemetry/core": "^2.8.0",
|
|
133
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
|
134
|
+
"@opentelemetry/resources": "^2.8.0",
|
|
135
|
+
"@opentelemetry/sdk-trace-node": "^2.8.0",
|
|
135
136
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
136
137
|
"@supabase/supabase-js": "^2.99.3",
|
|
137
138
|
"cheerio": "^1.2.0",
|