prism-mcp-server 12.5.7 → 13.0.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.
@@ -47,16 +47,26 @@ export async function searchYahooFree(query, limit = 5) {
47
47
  * and converts it to Markdown using Turndown.
48
48
  */
49
49
  export async function scrapeArticleLocal(url) {
50
- // SSRF protection: reject private/internal URLs
50
+ // SSRF protection: reject private/internal URLs.
51
+ // Set PRISM_DEV_MODE=1 to allow loopback/private hosts during local dev
52
+ // (testing against a local docs server, internal wiki, etc.). The flag
53
+ // is intentionally OFF in production deploys.
54
+ const devMode = process.env.PRISM_DEV_MODE === '1' || process.env.NODE_ENV === 'development';
51
55
  try {
52
56
  const parsed = new URL(url);
53
57
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
54
58
  throw new Error('Invalid protocol');
55
59
  const host = parsed.hostname.toLowerCase();
56
- if (host === 'localhost' || host === '127.0.0.1' || host === '::1' ||
57
- host.startsWith('10.') || host.startsWith('192.168.') || host.startsWith('169.254.') ||
58
- /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith('.internal') || host.endsWith('.local')) {
59
- throw new Error('Internal URLs not allowed');
60
+ const isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '::1';
61
+ const isPrivate = host.startsWith('10.') || host.startsWith('192.168.') || host.startsWith('169.254.') ||
62
+ /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith('.internal') || host.endsWith('.local');
63
+ if (isLoopback && !devMode) {
64
+ throw new Error('Loopback URLs not allowed in production (set PRISM_DEV_MODE=1 to allow)');
65
+ }
66
+ if (isPrivate) {
67
+ // Private RFC1918 ranges are never allowed — even in dev mode they
68
+ // can cross into other tenants' machines on a shared LAN.
69
+ throw new Error('Private network URLs not allowed');
60
70
  }
61
71
  }
62
72
  catch (e) {
@@ -128,7 +128,17 @@ export async function prepareSyncPacket(sourceId, targetPeerId, entries, sharedS
128
128
  export function receiveSyncPacket(encrypted, sharedSecret) {
129
129
  const key = deriveKey(sharedSecret);
130
130
  const decrypted = decrypt(encrypted, key);
131
- const payload = JSON.parse(decrypted);
131
+ // Wrap JSON.parse so a malformed payload from a misbehaving peer doesn't
132
+ // crash the receiver with an unhandled SyntaxError. We re-throw with a
133
+ // typed message so callers can distinguish corruption from auth failures.
134
+ let payload;
135
+ try {
136
+ payload = JSON.parse(decrypted);
137
+ }
138
+ catch (e) {
139
+ const msg = e instanceof Error ? e.message : String(e);
140
+ throw new Error(`Sync payload corrupted: invalid JSON (${msg})`);
141
+ }
132
142
  if (!verifySyncPayload(payload)) {
133
143
  throw new Error("Sync payload integrity check failed — checksum mismatch");
134
144
  }
@@ -0,0 +1,148 @@
1
+ export const ADAPTIVE_GET_PROFILE_TOOL = {
2
+ name: "adaptive_get_profile",
3
+ description: "Get the user's current adaptive profile and a compact signals snapshot. " +
4
+ "Use this when you need the user's dominant mood, motor rhythm, noise " +
5
+ "environment, or vocabulary preferences — typically before generating a " +
6
+ "response that should match their state. Returns the full profile plus a " +
7
+ "small `signals` block that is safe to embed in a prompt.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ user_id: {
12
+ type: "string",
13
+ description: "Optional. User identifier. If omitted, the server uses the authenticated user from context.",
14
+ },
15
+ include_history: {
16
+ type: "boolean",
17
+ description: "If true, include the toneHistory and full timeOfDayPatterns. Defaults to false (signals + summary only) to keep payload small.",
18
+ },
19
+ },
20
+ },
21
+ };
22
+ export const ADAPTIVE_SET_PROFILE_TOOL = {
23
+ name: "adaptive_set_profile",
24
+ description: "Replace the user's adaptive profile in full. Intended for caregivers " +
25
+ "syncing across devices, restoring from backup, or admin migration. " +
26
+ "Schema must match version 2 of the AdaptiveProfile (see " +
27
+ "src/shared/adaptiveEngine.ts).",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ user_id: {
32
+ type: "string",
33
+ description: "Optional. User identifier (defaults to authenticated user).",
34
+ },
35
+ profile: {
36
+ type: "object",
37
+ description: "Full AdaptiveProfile object. Must include version: 2.",
38
+ },
39
+ },
40
+ required: ["profile"],
41
+ },
42
+ };
43
+ export const ADAPTIVE_RECORD_EVENT_TOOL = {
44
+ name: "adaptive_record_event",
45
+ description: "Record a single behavioral event into the adaptive profile. " +
46
+ "Use this from any surface that observes user behavior (voice agents, " +
47
+ "AAC dwell triggers, message logs). Events are written incrementally — " +
48
+ "no need to fetch+modify+save the whole profile. " +
49
+ "Event types: " +
50
+ "tone (text → AdaptiveTone, also records to toneHistory), " +
51
+ "dwell (dwellMs sample for motor rhythm), " +
52
+ "move_speed (px/sec sample for cursor smoothing), " +
53
+ "noise (rmsDb sample for environment), " +
54
+ "message (text + optional categoryId for vocab + frequency tracking), " +
55
+ "mispronunciation (heard → intended; emergency words always pass through).",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ user_id: { type: "string", description: "Optional user id." },
60
+ event: {
61
+ type: "string",
62
+ enum: ["tone", "dwell", "move_speed", "noise", "message", "mispronunciation"],
63
+ description: "Event type.",
64
+ },
65
+ text: {
66
+ type: "string",
67
+ description: "For tone/message: the utterance text. For mispronunciation: the heard form.",
68
+ },
69
+ intended: {
70
+ type: "string",
71
+ description: "For mispronunciation: the corrected form.",
72
+ },
73
+ value: {
74
+ type: "number",
75
+ description: "For dwell/move_speed/noise: the numeric sample.",
76
+ },
77
+ category_id: {
78
+ type: "string",
79
+ description: "For message: optional category id (e.g. 'food', 'feelings').",
80
+ },
81
+ },
82
+ required: ["event"],
83
+ },
84
+ };
85
+ export const ADAPTIVE_DETECT_TONE_TOOL = {
86
+ name: "adaptive_detect_tone",
87
+ description: "Detect emotional tone from a piece of text WITHOUT recording it. " +
88
+ "Pure function — useful when you want to route a response (e.g. choose " +
89
+ "a TTS voice style or shape an LLM system prompt) but don't want to " +
90
+ "perturb the user's profile. Returns one of: " +
91
+ "neutral | friendly | excited | empathetic | serious. Emergency words " +
92
+ "(help/hurt/scared/911/etc) always map to 'serious'.",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ text: { type: "string", description: "Text to analyze." },
97
+ },
98
+ required: ["text"],
99
+ },
100
+ };
101
+ export const ADAPTIVE_RESET_TOOL = {
102
+ name: "adaptive_reset",
103
+ description: "Wipe the user's adaptive profile. Caregiver-initiated reset only — " +
104
+ "resets all learned dwell, motor speed, vocabulary, mispronunciation " +
105
+ "corrections, tone history, and noise calibration. Returns the fresh " +
106
+ "default profile.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ user_id: { type: "string", description: "Optional user id." },
111
+ confirm: {
112
+ type: "boolean",
113
+ description: "Must be true. Defends against accidental reset.",
114
+ },
115
+ },
116
+ required: ["confirm"],
117
+ },
118
+ };
119
+ // ─── Type guards (mirror the patterns used elsewhere in this repo) ───
120
+ export function isAdaptiveGetProfileArgs(args) {
121
+ return typeof args === "object" && args !== null;
122
+ }
123
+ export function isAdaptiveSetProfileArgs(args) {
124
+ return (typeof args === "object" &&
125
+ args !== null &&
126
+ "profile" in args &&
127
+ typeof args.profile === "object" &&
128
+ args.profile !== null);
129
+ }
130
+ export function isAdaptiveRecordEventArgs(args) {
131
+ if (typeof args !== "object" || args === null)
132
+ return false;
133
+ const a = args;
134
+ return (typeof a.event === "string" &&
135
+ ["tone", "dwell", "move_speed", "noise", "message", "mispronunciation"].includes(a.event));
136
+ }
137
+ export function isAdaptiveDetectToneArgs(args) {
138
+ return (typeof args === "object" &&
139
+ args !== null &&
140
+ "text" in args &&
141
+ typeof args.text === "string");
142
+ }
143
+ export function isAdaptiveResetArgs(args) {
144
+ return (typeof args === "object" &&
145
+ args !== null &&
146
+ "confirm" in args &&
147
+ args.confirm === true);
148
+ }
@@ -787,21 +787,65 @@ export async function sessionLoadContextHandler(args) {
787
787
  // agent loads its rules/conventions automatically at session start.
788
788
  let skillBlock = "";
789
789
  let skillLoaded = false;
790
+ const loadedSkills = [];
790
791
  if (effectiveRole) {
791
792
  const skillContent = await getSetting(`skill:${effectiveRole}`, "");
792
793
  if (skillContent && skillContent.trim()) {
793
794
  skillBlock = `\n\n[📜 ROLE SKILL: ${effectiveRole}]\n${skillContent.trim()}`;
794
795
  skillLoaded = true;
796
+ loadedSkills.push(effectiveRole);
795
797
  debugLog(`[session_load_context] Injecting skill for role="${effectiveRole}" (${skillContent.length} chars)`);
796
798
  }
797
799
  }
800
+ // ─── Project-Aware Skill Injection ──────────────────────────
801
+ // Skill routing (which skills load for which project) is the SINGLE
802
+ // SOURCE OF TRUTH at synalux: /api/v1/skills/routing. We pull the
803
+ // canonical table on every session and resolve locally. Skill CONTENT
804
+ // continues to be stored in this server's settings under skill:<name>
805
+ // — synalux owns the WHICH, this server owns the WHAT, no duplication
806
+ // of the routing config in three repos.
807
+ const { resolveSkillsForProject } = await import("./skillRouting.js");
808
+ const skillsToLoad = await resolveSkillsForProject(project);
809
+ for (const skillName of skillsToLoad) {
810
+ if (loadedSkills.includes(skillName))
811
+ continue;
812
+ const content = await getSetting(`skill:${skillName}`, "");
813
+ if (content && content.trim()) {
814
+ skillBlock += `\n\n[📜 SKILL: ${skillName}]\n${content.trim()}`;
815
+ loadedSkills.push(skillName);
816
+ skillLoaded = true;
817
+ debugLog(`[session_load_context] Skill "${skillName}" loaded for project="${project}"`);
818
+ }
819
+ }
820
+ // ─── Memory-Based Skill Discovery ──────────────────────────
821
+ // If recent handoff/ledger mentions a skill name, auto-load it.
822
+ // This lets the agent's own memory drive skill activation.
823
+ if (formattedContext.length > 0) {
824
+ const contextText = formattedContext.toLowerCase();
825
+ const allSkillKeys = await storage.getAllSettings?.() || {};
826
+ for (const [k, v] of Object.entries(allSkillKeys)) {
827
+ if (!k.startsWith("skill:") || !v)
828
+ continue;
829
+ const skillName = k.replace("skill:", "");
830
+ if (loadedSkills.includes(skillName))
831
+ continue;
832
+ // Only load if the skill name appears in recent context
833
+ if (contextText.includes(skillName.replace(/-/g, " ")) || contextText.includes(skillName)) {
834
+ skillBlock += `\n\n[📜 CONTEXT SKILL: ${skillName}]\n${v}`;
835
+ loadedSkills.push(skillName);
836
+ debugLog(`[session_load_context] Context-triggered skill "${skillName}"`);
837
+ }
838
+ }
839
+ }
798
840
  // ─── Agent Greeting Block ────────────────────────────────────
799
841
  // Shows agent identity (name + role) and skill status after briefing.
800
842
  let greetingBlock = "";
801
843
  if (agentName || effectiveRole) {
802
844
  const namePart = agentName ? `👋 **${agentName}**` : `👋 **Agent**`;
803
845
  const rolePart = effectiveRole ? ` · Role: \`${effectiveRole}\`` : "";
804
- const skillPart = skillLoaded ? ` · 📜 \`${effectiveRole}\` skill loaded` : (effectiveRole ? " · 📜 No skill configured" : "");
846
+ const skillPart = loadedSkills.length > 0
847
+ ? ` · 📜 Skills: ${loadedSkills.map(s => `\`${s}\``).join(", ")}`
848
+ : (effectiveRole ? " · 📜 No skill configured" : "");
805
849
  greetingBlock = `\n\n[👤 AGENT IDENTITY]\n${namePart}${rolePart}${skillPart}`;
806
850
  }
807
851
  // ─── SDM Intuitive Recall (v5.5) ───
@@ -984,7 +1028,8 @@ export async function sessionSaveImageHandler(args) {
984
1028
  const resolvedPath = nodePath.resolve(file_path);
985
1029
  const home = os.homedir();
986
1030
  const cwd = process.cwd();
987
- if (!resolvedPath.startsWith(home) && !resolvedPath.startsWith(cwd) && !resolvedPath.startsWith('/tmp')) {
1031
+ const tmpDir = os.tmpdir();
1032
+ if (!resolvedPath.startsWith(home) && !resolvedPath.startsWith(cwd) && !resolvedPath.startsWith('/tmp') && !resolvedPath.startsWith(tmpDir)) {
988
1033
  return {
989
1034
  content: [{ type: "text", text: "Error: file_path must be within your home directory, project directory, or /tmp." }],
990
1035
  isError: true,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Skill routing client — fetches the canonical routing table from synalux.
3
+ *
4
+ * Single source of truth lives in synalux at /api/v1/skills/routing.
5
+ * This module:
6
+ * 1. Caches the response in-memory for 5 minutes (matches synalux's
7
+ * Cache-Control s-maxage).
8
+ * 2. Falls back to a tiny offline default if synalux is unreachable, so
9
+ * free-tier / disconnected installations still get the BCBA universal
10
+ * skill loaded.
11
+ *
12
+ * To change the routing for production, edit
13
+ * synalux-private/portal/src/app/api/v1/skills/routing/route.ts
14
+ * and deploy synalux. prism-mcp picks up the new config within 5 minutes.
15
+ *
16
+ * Do NOT add hardcoded skill names here outside the OFFLINE_FALLBACK block
17
+ * — that defeats the single-source-of-truth design.
18
+ */
19
+ // Minimal fallback when synalux is unreachable. Only the universal BCBA
20
+ // skill — project-specific mappings need synalux to resolve.
21
+ const OFFLINE_FALLBACK = {
22
+ version: 1,
23
+ universal: ['bcba_ai_assistant'],
24
+ projects: {},
25
+ };
26
+ const SYNALUX_BASE = process.env.SYNALUX_BASE_URL || 'https://synalux.ai';
27
+ const CACHE_TTL_MS = 5 * 60 * 1000;
28
+ let cached = null;
29
+ let inflight = null;
30
+ async function fetchOnce() {
31
+ try {
32
+ const res = await fetch(`${SYNALUX_BASE}/api/v1/skills/routing`, {
33
+ headers: { Accept: 'application/json' },
34
+ // Routing is on every session_load_context, must not block long.
35
+ signal: AbortSignal.timeout(2_500),
36
+ });
37
+ if (!res.ok)
38
+ throw new Error(`HTTP ${res.status}`);
39
+ const body = (await res.json());
40
+ if (typeof body !== 'object' ||
41
+ body == null ||
42
+ typeof body.version !== 'number' ||
43
+ !Array.isArray(body.universal) ||
44
+ typeof body.projects !== 'object') {
45
+ throw new Error('malformed routing table');
46
+ }
47
+ return body;
48
+ }
49
+ catch {
50
+ return OFFLINE_FALLBACK;
51
+ }
52
+ }
53
+ /**
54
+ * Resolve the skill list for a given project (case-insensitive substring
55
+ * match against the routing table). Always returns at least the universal
56
+ * skills.
57
+ */
58
+ export async function resolveSkillsForProject(project) {
59
+ const now = Date.now();
60
+ if (!cached || now - cached.fetchedAt > CACHE_TTL_MS) {
61
+ if (!inflight) {
62
+ inflight = fetchOnce().then((table) => {
63
+ cached = { table, fetchedAt: Date.now() };
64
+ return table;
65
+ }).finally(() => { inflight = null; });
66
+ }
67
+ await inflight;
68
+ }
69
+ const table = cached.table;
70
+ const out = new Set(table.universal);
71
+ const projectLower = project.toLowerCase();
72
+ for (const [pattern, skills] of Object.entries(table.projects)) {
73
+ if (projectLower.includes(pattern)) {
74
+ for (const s of skills)
75
+ out.add(s);
76
+ }
77
+ }
78
+ return Array.from(out);
79
+ }
80
+ /** Force a re-fetch on the next call. Exposed for tests + admin tooling. */
81
+ export function _invalidateRoutingCache() {
82
+ cached = null;
83
+ inflight = null;
84
+ }
85
+ /** Test/debug only — read the OFFLINE_FALLBACK constant. */
86
+ export const _OFFLINE_FALLBACK = OFFLINE_FALLBACK;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "12.5.7",
3
+ "version": "13.0.0",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
5
  "description": "Prism-Coder v12.5: The world's first O(1) Cognitive Memory Architecture for AI Agents. 100% Tool-Call Accuracy (BFCL Gold Certified), 54 Agent Skills, Zero-Search Retrieval (HDC/HRR), HIPAA-hardened local-first storage, and SLERP-optimized GRPO alignment.",
6
6
  "module": "index.ts",