prism-mcp-server 20.2.0 → 20.2.2
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 +137 -15
- package/dist/cli.js +222 -34
- package/dist/config.js +14 -7
- package/dist/connect.js +854 -16
- package/dist/dashboard/server.js +9 -14
- package/dist/dashboard/settingsPolicy.js +41 -0
- package/dist/localFirstPolicy.js +20 -0
- package/dist/onboarding/wizard.js +3 -6
- package/dist/server.js +77 -65
- package/dist/session/sessionContext.js +5 -3
- package/dist/skillManifestSync.js +943 -0
- package/dist/storage/configStorage.js +110 -1
- package/dist/storage/index.js +15 -4
- package/dist/storage/inferMetricsLedger.js +89 -16
- package/dist/storage/panelMetricsSpool.js +324 -0
- package/dist/storage/sqlite.js +1 -1
- package/dist/storage/synalux.js +18 -1
- package/dist/tools/__tests__/ledgerHandlers.test.js +13 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/ledgerHandlers.js +468 -53
- package/dist/tools/prismInferHandler.js +171 -12
- package/dist/tools/sessionMemoryDefinitions.js +51 -10
- package/dist/tools/skillRouting.js +39 -7
- package/dist/tools/taskRouterHandler.js +184 -29
- package/dist/utils/entitlements.js +6 -2
- package/dist/utils/inferenceMetrics.js +22 -3
- package/dist/utils/modelPicker.js +3 -3
- package/dist/utils/synaluxJwt.js +8 -3
- package/package.json +6 -2
package/dist/dashboard/server.js
CHANGED
|
@@ -32,6 +32,7 @@ import { getLLMProvider } from "../utils/llm/factory.js";
|
|
|
32
32
|
import { buildVaultDirectory } from "../utils/vaultExporter.js";
|
|
33
33
|
import { redactSettings } from "../tools/commonHelpers.js";
|
|
34
34
|
import { handleGraphRoutes } from "./graphRouter.js";
|
|
35
|
+
import { isDashboardSettingKeyAllowed, isDashboardSettingValueAllowed } from "./settingsPolicy.js";
|
|
35
36
|
import { safeCompare, generateToken, isAuthenticated, createRateLimiter, initJWKS, } from "./authUtils.js";
|
|
36
37
|
const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
|
|
37
38
|
/** Read HTTP request body as string (Buffer-based to avoid GC thrash on large imports) */
|
|
@@ -633,26 +634,20 @@ return false;}
|
|
|
633
634
|
try {
|
|
634
635
|
const body = await readBody(req);
|
|
635
636
|
const parsed = JSON.parse(body);
|
|
636
|
-
if (parsed.key && parsed.value !== undefined) {
|
|
637
|
+
if (typeof parsed.key === "string" && parsed.key && parsed.value !== undefined) {
|
|
637
638
|
// SECURITY: Allowlist of dashboard-settable keys to prevent
|
|
638
639
|
// credential overwrite (SUPABASE_KEY, STRIPE_SECRET_KEY, etc.)
|
|
639
|
-
|
|
640
|
-
"PRISM_STORAGE",
|
|
641
|
-
"embedding_provider", "embedding_model",
|
|
642
|
-
"PRISM_ENABLE_HIVEMIND", "PRISM_DARK_FACTORY_ENABLED",
|
|
643
|
-
"PRISM_TASK_ROUTER_ENABLED", "PRISM_SCHOLAR_ENABLED",
|
|
644
|
-
"PRISM_HDC_ENABLED", "PRISM_ACTR_ENABLED",
|
|
645
|
-
"PRISM_GRAPH_PRUNING_ENABLED",
|
|
646
|
-
]);
|
|
647
|
-
const isSkillKey = parsed.key.startsWith("skill:");
|
|
648
|
-
const isTTLKey = parsed.key.startsWith("ttl:");
|
|
649
|
-
const isAutoloadKey = parsed.key.startsWith("autoload:");
|
|
650
|
-
if (!SETTABLE_KEYS.has(parsed.key) && !isSkillKey && !isTTLKey && !isAutoloadKey) {
|
|
640
|
+
if (!isDashboardSettingKeyAllowed(parsed.key)) {
|
|
651
641
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
652
642
|
return res.end(JSON.stringify({ error: `Setting key "${parsed.key}" is not allowed via the dashboard.` }));
|
|
653
643
|
}
|
|
644
|
+
const value = String(parsed.value);
|
|
645
|
+
if (!isDashboardSettingValueAllowed(parsed.key, value)) {
|
|
646
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
647
|
+
return res.end(JSON.stringify({ error: `Invalid value for setting "${parsed.key}".` }));
|
|
648
|
+
}
|
|
654
649
|
const { setSetting } = await import("../storage/configStorage.js");
|
|
655
|
-
await setSetting(parsed.key,
|
|
650
|
+
await setSetting(parsed.key, value);
|
|
656
651
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
657
652
|
return res.end(JSON.stringify({ ok: true, key: parsed.key, value: parsed.value }));
|
|
658
653
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const DASHBOARD_SETTABLE_KEYS = new Set([
|
|
2
|
+
"PRISM_STORAGE",
|
|
3
|
+
"dashboard_theme", "default_context_depth", "max_tokens",
|
|
4
|
+
"default_role", "agent_name", "autoload_projects",
|
|
5
|
+
"auto_capture", "hivemind_enabled", "task_router_enabled",
|
|
6
|
+
"embedding_provider", "embedding_model",
|
|
7
|
+
"PRISM_ENABLE_HIVEMIND", "PRISM_DARK_FACTORY_ENABLED",
|
|
8
|
+
"PRISM_TASK_ROUTER_ENABLED", "PRISM_SCHOLAR_ENABLED",
|
|
9
|
+
"PRISM_HDC_ENABLED", "PRISM_ACTR_ENABLED",
|
|
10
|
+
"PRISM_GRAPH_PRUNING_ENABLED",
|
|
11
|
+
]);
|
|
12
|
+
/** Restrict the generic settings endpoint to documented dashboard controls. */
|
|
13
|
+
export function isDashboardSettingKeyAllowed(key) {
|
|
14
|
+
return DASHBOARD_SETTABLE_KEYS.has(key) || key.startsWith("ttl:") ||
|
|
15
|
+
key.startsWith("autoload:") || key.startsWith("repo_path:");
|
|
16
|
+
}
|
|
17
|
+
/** Validate settings that later enter tool descriptions, paths, or numeric limits. */
|
|
18
|
+
export function isDashboardSettingValueAllowed(key, value) {
|
|
19
|
+
if (value.length > 4096 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value))
|
|
20
|
+
return false;
|
|
21
|
+
if (key === "default_context_depth")
|
|
22
|
+
return ["quick", "standard", "deep"].includes(value);
|
|
23
|
+
if (key === "dashboard_theme")
|
|
24
|
+
return ["dark", "midnight", "purple"].includes(value);
|
|
25
|
+
if (key === "PRISM_STORAGE")
|
|
26
|
+
return ["auto", "local", "supabase"].includes(value);
|
|
27
|
+
if (key === "max_tokens")
|
|
28
|
+
return /^\d+$/.test(value) && Number(value) <= 100_000;
|
|
29
|
+
if (["auto_capture", "hivemind_enabled", "task_router_enabled"].includes(key)) {
|
|
30
|
+
return value === "true" || value === "false";
|
|
31
|
+
}
|
|
32
|
+
if (key === "agent_name")
|
|
33
|
+
return value.length <= 80 && !/[<>]/.test(value);
|
|
34
|
+
if (key === "default_role")
|
|
35
|
+
return /^[a-zA-Z0-9._:-]{0,64}$/.test(value);
|
|
36
|
+
if (key === "autoload_projects") {
|
|
37
|
+
const projects = value.split(",").map((project) => project.trim()).filter(Boolean);
|
|
38
|
+
return projects.length <= 20 && projects.every((project) => /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(project));
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical local-first orchestration contract shared by every MCP host.
|
|
3
|
+
*
|
|
4
|
+
* Keep this policy host-neutral. Claude Code, Gemini CLI, and Codex also get
|
|
5
|
+
* the same text in their native instruction files; Cursor and Claude Desktop
|
|
6
|
+
* receive it through the MCP initialize instructions and tool metadata.
|
|
7
|
+
*/
|
|
8
|
+
export const LOCAL_FIRST_POLICY_LINES = [
|
|
9
|
+
"## Prism local-first orchestration",
|
|
10
|
+
"For bounded, verifiable delegated work, use Prism's local worker before any host-native or background subagent.",
|
|
11
|
+
"When session_task_route is available, call it first. If it returns target `claw`, call `prism_infer` with the",
|
|
12
|
+
"recommended arguments, `cloud_fallback: false`, and the `project` and `conversation_id` from this session when known.",
|
|
13
|
+
"Prism loads that project's dashboard-configured quick, standard, or deep memory and selects a RAM-safe local model.",
|
|
14
|
+
"Never create host-native or background subagents for routine work, never fan out, and never nest agents.",
|
|
15
|
+
"If local inference is unavailable, refused, degraded, or the task requires host tools or reserved judgment, continue",
|
|
16
|
+
"in the current host thread. A host-native subagent is a last resort: at most one, no nesting, using the configured",
|
|
17
|
+
"economy model. The current host remains responsible for verifying local output before using it.",
|
|
18
|
+
];
|
|
19
|
+
export const LOCAL_FIRST_POLICY_ID = "local-first";
|
|
20
|
+
export const LOCAL_FIRST_POLICY_TEXT = LOCAL_FIRST_POLICY_LINES.join(" ");
|
|
@@ -142,14 +142,11 @@ function getFirstSearchContent() {
|
|
|
142
142
|
instructions: [
|
|
143
143
|
"Start a fresh conversation with your AI agent",
|
|
144
144
|
'Ask: "What did we set up in our last session?"',
|
|
145
|
-
"The agent will call
|
|
145
|
+
"The agent will call session_bootstrap before answering",
|
|
146
146
|
"You should see your saved memory returned!",
|
|
147
147
|
],
|
|
148
|
-
codeSnippet: `// The agent calls:
|
|
149
|
-
|
|
150
|
-
project: "my-first-project",
|
|
151
|
-
level: "standard"
|
|
152
|
-
})
|
|
148
|
+
codeSnippet: `// The agent calls once with no project or depth override:
|
|
149
|
+
session_bootstrap({})
|
|
153
150
|
// → Returns your saved summary + any open TODOs`,
|
|
154
151
|
nextStep: "advanced_tour",
|
|
155
152
|
progress: 71,
|
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* HOW MCP WORKS (simplified):
|
|
20
20
|
* 1. The AI client (e.g., Claude) connects via stdin/stdout
|
|
21
|
-
* 2. On connect, the client receives our capabilities (tools + prompts + resources)
|
|
21
|
+
* 2. On connect, the client receives our capabilities (tools + prompts + resources + logging)
|
|
22
22
|
* 3. The client can:
|
|
23
23
|
* - Call tools (brave_web_search, session_save_ledger, etc.)
|
|
24
24
|
* - List/get prompts (/resume_session slash command)
|
|
@@ -83,10 +83,12 @@ import { ddInfo, ddError as ddLogError } from "./utils/ddLogger.js";
|
|
|
83
83
|
import { inferenceMetricsHandler } from "./utils/inferenceMetrics.js";
|
|
84
84
|
import { recordInvocation } from "./utils/analytics.js";
|
|
85
85
|
import { BOUNDARIES_TEXT } from "./boundaries/boundaries.js";
|
|
86
|
+
import { triggerSkillManifestSync } from "./skillManifestSync.js";
|
|
87
|
+
import { LOCAL_FIRST_POLICY_TEXT } from "./localFirstPolicy.js";
|
|
86
88
|
// ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
|
|
87
89
|
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";
|
|
88
90
|
// Session memory tools — only used if Supabase is configured
|
|
89
|
-
import { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL,
|
|
91
|
+
import { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, SESSION_BOOTSTRAP_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL,
|
|
90
92
|
// ─── v0.4.0: New tool definitions (Enhancements #2 and #4) ───
|
|
91
93
|
SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL,
|
|
92
94
|
// ─── v2.0: Time Travel tool definitions ───
|
|
@@ -118,7 +120,7 @@ ONBOARDING_WIZARD_TOOL, EXTRACT_ENTITIES_TOOL, API_ANALYTICS_TOOL, BACKUP_DATABA
|
|
|
118
120
|
// v15.5: Knowledge Ingestion
|
|
119
121
|
KNOWLEDGE_INGEST_TOOL,
|
|
120
122
|
// v19.2: Inference Metrics
|
|
121
|
-
INFERENCE_METRICS_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
|
|
123
|
+
INFERENCE_METRICS_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, sessionBootstrapHandler, knowledgeSearchHandler, knowledgeForgetHandler,
|
|
122
124
|
// ─── v0.4.0: New tool handlers ───
|
|
123
125
|
compactLedgerHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, sessionBackfillLinksHandler, sessionSynthesizeEdgesHandler, sessionCognitiveRouteHandler,
|
|
124
126
|
// ─── v2.0: Time Travel handlers ───
|
|
@@ -174,40 +176,16 @@ const BASE_TOOLS = [
|
|
|
174
176
|
RESEARCH_PAPER_ANALYSIS_TOOL, // gemini_research_paper_analysis — paper analysis
|
|
175
177
|
PRISM_INFER_TOOL, // prism_infer — local-first inference (token-saving cascade)
|
|
176
178
|
];
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
// sendLoggingMessage are NOT reliably surfaced by any client.
|
|
183
|
-
//
|
|
184
|
-
// buildSessionMemoryTools() is called inside createServer() AFTER
|
|
185
|
-
// initConfigStorage() has run, so getSettingSync() returns real
|
|
186
|
-
// dashboard config values.
|
|
187
|
-
function buildSessionMemoryTools(autoloadList) {
|
|
188
|
-
// Dynamically inject auto-load instruction into session_load_context description.
|
|
189
|
-
// CRITICAL: The instruction is PREPENDED so it appears FIRST in the tool description.
|
|
190
|
-
// LLMs read top-to-bottom — appending the instruction at the end caused it to be
|
|
191
|
-
// deprioritized and skipped by both Antigravity and Claude CLI.
|
|
192
|
-
let loadContextTool = SESSION_LOAD_CONTEXT_TOOL;
|
|
193
|
-
if (autoloadList.length > 0) {
|
|
194
|
-
const projectList = autoloadList.join(", ");
|
|
195
|
-
// Read the preferred context depth from dashboard config (defaults to "standard")
|
|
196
|
-
const defaultLevel = getSettingSync("default_context_depth", "standard");
|
|
197
|
-
const autoLoadPrefix = `IMPORTANT: At the start of every conversation, you MUST call this tool ` +
|
|
198
|
-
`for each of these projects: ${projectList}. Do this before any other work. ` +
|
|
199
|
-
`Load level should be "${defaultLevel}" unless the user specifies otherwise. ` +
|
|
200
|
-
`Do NOT skip this. Do NOT claim the tool is unavailable. The tool IS available — just call it.\n` +
|
|
201
|
-
`Then display the agent identity from the response: \`🤖 Agent: <role> — <name>\`\n\n`;
|
|
202
|
-
loadContextTool = {
|
|
203
|
-
...SESSION_LOAD_CONTEXT_TOOL,
|
|
204
|
-
description: autoLoadPrefix + SESSION_LOAD_CONTEXT_TOOL.description,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
179
|
+
// Hook-free first-turn startup is owned by session_bootstrap. Its static tool
|
|
180
|
+
// description is surfaced by native MCP hosts before any tool call and keeps
|
|
181
|
+
// project, depth, identity, and role selection inside dashboard configuration.
|
|
182
|
+
// session_load_context remains available for explicit reloads and legacy hosts.
|
|
183
|
+
function buildSessionMemoryTools() {
|
|
207
184
|
return [
|
|
185
|
+
SESSION_BOOTSTRAP_TOOL, // session_bootstrap — hook-free configured first-turn greeting + context
|
|
208
186
|
SESSION_SAVE_LEDGER_TOOL, // session_save_ledger — append immutable session log
|
|
209
187
|
SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
|
|
210
|
-
|
|
188
|
+
SESSION_LOAD_CONTEXT_TOOL, // session_load_context — explicit project reload / legacy fallback
|
|
211
189
|
KNOWLEDGE_SEARCH_TOOL, // knowledge_search — search accumulated knowledge
|
|
212
190
|
KNOWLEDGE_FORGET_TOOL, // knowledge_forget — prune bad/old memories
|
|
213
191
|
SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
|
|
@@ -272,11 +250,9 @@ const activeSubscriptions = new Set();
|
|
|
272
250
|
let storageReady = null;
|
|
273
251
|
let storageIsReady = false;
|
|
274
252
|
// ─── v5.2.1: Deferred Auto-Push Tracking ─────────────────────
|
|
275
|
-
// Tracks whether any client has already called
|
|
276
|
-
// Used by
|
|
277
|
-
//
|
|
278
|
-
// seconds) is never affected, while Antigravity gets a fallback push
|
|
279
|
-
// when the model fails to comply with auto-load instructions.
|
|
253
|
+
// Tracks whether any client has already called session_bootstrap or
|
|
254
|
+
// session_load_context. Used by deferred auto-push to avoid duplicates when a
|
|
255
|
+
// native host complies with the hook-free first-turn instruction.
|
|
280
256
|
let contextLoadedByClient = false;
|
|
281
257
|
/**
|
|
282
258
|
* Notifies subscribed clients that a resource has changed.
|
|
@@ -304,11 +280,14 @@ export function notifyResourceUpdate(project, server) {
|
|
|
304
280
|
* - prompts: /resume_session — inject previous context before LLM thinks
|
|
305
281
|
* - resources: memory://{project}/handoff — paperclip-attachable session state
|
|
306
282
|
* with subscribe support for live refresh
|
|
283
|
+
* - logging: Diagnostic notifications for auto-push, Telepathy, and watchdog
|
|
284
|
+
* events. Clients may hide these; logging does not guarantee a
|
|
285
|
+
* visible assistant greeting.
|
|
307
286
|
*/
|
|
308
287
|
export function getAllPossibleTools() {
|
|
309
288
|
return [
|
|
310
289
|
...BASE_TOOLS,
|
|
311
|
-
...buildSessionMemoryTools(
|
|
290
|
+
...buildSessionMemoryTools(),
|
|
312
291
|
...AGENT_REGISTRY_TOOLS,
|
|
313
292
|
SESSION_TASK_ROUTE_TOOL,
|
|
314
293
|
SESSION_START_PIPELINE_TOOL,
|
|
@@ -317,15 +296,8 @@ export function getAllPossibleTools() {
|
|
|
317
296
|
];
|
|
318
297
|
}
|
|
319
298
|
export function getAvailableTools() {
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
// (available after initConfigStorage() in startServer).
|
|
323
|
-
//
|
|
324
|
-
// ARCHITECTURE DECISION: We inject the auto-load instruction into
|
|
325
|
-
// the session_load_context TOOL DESCRIPTION, not into `instructions`
|
|
326
|
-
// or `sendLoggingMessage`. Tool descriptions are the ONLY mechanism
|
|
327
|
-
// guaranteed by ALL MCP clients (Antigravity, Claude Code, Claude CLI).
|
|
328
|
-
//
|
|
299
|
+
// Read auto-load projects exclusively from dashboard config so startup
|
|
300
|
+
// diagnostics and session_bootstrap share one source of truth.
|
|
329
301
|
// The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
|
|
330
302
|
// is the single source of truth. This prevents mismatches between
|
|
331
303
|
// env var and dashboard settings causing duplicate project loads.
|
|
@@ -335,7 +307,7 @@ export function getAvailableTools() {
|
|
|
335
307
|
if (autoloadList.length > 0) {
|
|
336
308
|
console.error(`[Prism] Auto-load projects (dashboard): ${autoloadList.join(', ')}`);
|
|
337
309
|
}
|
|
338
|
-
const SESSION_MEMORY_TOOLS = buildSessionMemoryTools(
|
|
310
|
+
const SESSION_MEMORY_TOOLS = buildSessionMemoryTools();
|
|
339
311
|
return [
|
|
340
312
|
...BASE_TOOLS,
|
|
341
313
|
...SESSION_MEMORY_TOOLS,
|
|
@@ -344,6 +316,20 @@ export function getAvailableTools() {
|
|
|
344
316
|
...(PRISM_DARK_FACTORY_ENABLED ? [SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL] : []),
|
|
345
317
|
];
|
|
346
318
|
}
|
|
319
|
+
export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI Agents. On the first user turn of every conversation, ` +
|
|
320
|
+
`including greetings, call session_bootstrap exactly once with {} before any user-facing response. ` +
|
|
321
|
+
`Emit no preamble. Print the complete tool result verbatim as the entire first-turn startup display, before any optional ` +
|
|
322
|
+
`answer. Do not summarize, paraphrase, rename headings, reformat, or omit any returned section. Preserve its order and ` +
|
|
323
|
+
`line content. For a greeting-only prompt, stop after the verbatim startup display. ` +
|
|
324
|
+
`Do not substitute session_load_context while session_bootstrap is available; use session_load_context ` +
|
|
325
|
+
`only for an explicit project reload or as an older-server fallback. ` +
|
|
326
|
+
`Use session_save_ledger to log completed work and session_save_handoff to preserve state for the next session. ` +
|
|
327
|
+
`Reuse the conversation_id returned by session_bootstrap in structuredContent for those saves and for ` +
|
|
328
|
+
`session_detect_drift, the 60-minute goal-alignment drift check. Do not add the id to the visible greeting.\n\n` +
|
|
329
|
+
`${LOCAL_FIRST_POLICY_TEXT}\n\n` +
|
|
330
|
+
`Architecture: session_save_ledger and session_save_handoff require context loaded by session_bootstrap ` +
|
|
331
|
+
`or session_load_context when a conversation_id is supplied. ${BOUNDARIES_TEXT} ` +
|
|
332
|
+
`All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`;
|
|
347
333
|
export function createServer() {
|
|
348
334
|
const ALL_TOOLS = getAvailableTools();
|
|
349
335
|
const server = new Server({
|
|
@@ -352,14 +338,15 @@ export function createServer() {
|
|
|
352
338
|
}, {
|
|
353
339
|
capabilities: {
|
|
354
340
|
tools: {},
|
|
341
|
+
logging: {},
|
|
355
342
|
// ─── v0.4.0: Prompt capability (Enhancement #1) ───
|
|
356
343
|
...(SESSION_MEMORY_ENABLED ? { prompts: {} } : {}),
|
|
357
344
|
// ─── v0.4.0: Resource capability (Enhancement #3) ───
|
|
358
345
|
...(SESSION_MEMORY_ENABLED ? { resources: { subscribe: true } } : {}),
|
|
359
346
|
},
|
|
360
|
-
// Supplementary signal
|
|
361
|
-
//
|
|
362
|
-
instructions:
|
|
347
|
+
// Supplementary signal for clients that surface MCP server instructions.
|
|
348
|
+
// The matching session_bootstrap tool description is the primary path.
|
|
349
|
+
instructions: PRISM_SERVER_INSTRUCTIONS,
|
|
363
350
|
});
|
|
364
351
|
// ── Handler: Initialize ──
|
|
365
352
|
// NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
|
|
@@ -753,6 +740,12 @@ export function createServer() {
|
|
|
753
740
|
contextLoadedByClient = true; // v5.2.1: suppress deferred auto-push
|
|
754
741
|
result = await sessionLoadContextHandler(args);
|
|
755
742
|
break;
|
|
743
|
+
case "session_bootstrap":
|
|
744
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
745
|
+
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
746
|
+
contextLoadedByClient = true;
|
|
747
|
+
result = await sessionBootstrapHandler(args);
|
|
748
|
+
break;
|
|
756
749
|
case "knowledge_search":
|
|
757
750
|
if (!SESSION_MEMORY_ENABLED)
|
|
758
751
|
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
@@ -1095,13 +1088,15 @@ export function createSandboxServer() {
|
|
|
1095
1088
|
}, {
|
|
1096
1089
|
capabilities: {
|
|
1097
1090
|
tools: {},
|
|
1091
|
+
logging: {},
|
|
1098
1092
|
prompts: {},
|
|
1099
1093
|
resources: { subscribe: true },
|
|
1100
1094
|
},
|
|
1095
|
+
instructions: PRISM_SERVER_INSTRUCTIONS,
|
|
1101
1096
|
});
|
|
1102
1097
|
// Register all tool listings unconditionally
|
|
1103
1098
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1104
|
-
tools: [...BASE_TOOLS, ...buildSessionMemoryTools(
|
|
1099
|
+
tools: [...BASE_TOOLS, ...buildSessionMemoryTools(), ...AGENT_REGISTRY_TOOLS, SESSION_TASK_ROUTE_TOOL, SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL],
|
|
1105
1100
|
}));
|
|
1106
1101
|
// Register prompts listing so scanners see resume_session
|
|
1107
1102
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
@@ -1234,6 +1229,26 @@ export async function startServer() {
|
|
|
1234
1229
|
const transport = new StdioServerTransport();
|
|
1235
1230
|
await server.connect(transport);
|
|
1236
1231
|
console.error(`[Prism] MCP Server successfully started and listening on stdio...`);
|
|
1232
|
+
// Start the authoritative tier-skill refresh only after the MCP transport is
|
|
1233
|
+
// connected. session_load_context awaits this same single-flight promise,
|
|
1234
|
+
// while the initialize handshake is never held behind portal I/O. The
|
|
1235
|
+
// recurring refresh enforces tier changes even in long-lived native hosts
|
|
1236
|
+
// that do not call session_load_context again.
|
|
1237
|
+
const runSkillManifestSync = () => {
|
|
1238
|
+
void triggerSkillManifestSync().then((result) => {
|
|
1239
|
+
if (result.status === "failed" || result.status === "partial") {
|
|
1240
|
+
console.error(`[Prism Skill Sync] Refresh failed; retained last-good skills: ${result.error}`);
|
|
1241
|
+
}
|
|
1242
|
+
else if (result.conflicts.length > 0) {
|
|
1243
|
+
console.error(`[Prism Skill Sync] Preserved unowned/modified native skill conflicts: ${result.conflicts.join(", ")}`);
|
|
1244
|
+
}
|
|
1245
|
+
}).catch((error) => {
|
|
1246
|
+
console.error(`[Prism Skill Sync] Unexpected refresh failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
1247
|
+
});
|
|
1248
|
+
};
|
|
1249
|
+
runSkillManifestSync();
|
|
1250
|
+
const skillManifestRefresh = setInterval(runSkillManifestSync, 5 * 60 * 1000);
|
|
1251
|
+
skillManifestRefresh.unref();
|
|
1237
1252
|
// Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
|
|
1238
1253
|
// The stdin close handler is critical — when MCP clients disconnect, they
|
|
1239
1254
|
// often just close the pipe without sending a signal, leaving zombie processes.
|
|
@@ -1255,21 +1270,18 @@ export async function startServer() {
|
|
|
1255
1270
|
]).catch(err => {
|
|
1256
1271
|
console.error(`[Prism] Storage pre-warm failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1257
1272
|
});
|
|
1258
|
-
// ───
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
// auto-load projects list. Tool descriptions are surfaced by ALL
|
|
1262
|
-
// MCP clients — this is the primary mechanism.
|
|
1273
|
+
// ─── Hook-free auto-load via session_bootstrap ─────────────
|
|
1274
|
+
// Static tool and MCP server instructions make the first-turn call
|
|
1275
|
+
// explicit while dashboard settings remain server-owned.
|
|
1263
1276
|
//
|
|
1264
1277
|
// ─── v5.2.1: Deferred Auto-Push (fallback for non-compliant models) ──
|
|
1265
1278
|
// After storage warms up, wait AUTOLOAD_PUSH_DELAY_MS. If the client
|
|
1266
|
-
// (model) hasn't
|
|
1279
|
+
// (model) hasn't loaded context through either tool by then, push via
|
|
1267
1280
|
// sendLoggingMessage as a last resort. This is a FALLBACK — it's not
|
|
1268
1281
|
// guaranteed to be surfaced by all clients, but it's better than nothing.
|
|
1269
1282
|
//
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
// within 5 seconds. 10s gives ample time for well-behaved clients.
|
|
1283
|
+
// Ten seconds gives native hosts time to honor the bootstrap instruction
|
|
1284
|
+
// while preserving the fallback for clients that do not.
|
|
1273
1285
|
const AUTOLOAD_PUSH_DELAY_MS = 10_000;
|
|
1274
1286
|
// Read autoload projects from dashboard config (same source as createServer)
|
|
1275
1287
|
const pushAutoloadList = getSettingSync("autoload_projects", "")
|
|
@@ -1279,12 +1291,12 @@ export async function startServer() {
|
|
|
1279
1291
|
storageReady?.then(async () => {
|
|
1280
1292
|
// Wait for the delay period to give the model a chance to call the tool
|
|
1281
1293
|
await new Promise(r => setTimeout(r, AUTOLOAD_PUSH_DELAY_MS));
|
|
1282
|
-
// If the client already
|
|
1294
|
+
// If the client already loaded context through either tool, skip it.
|
|
1283
1295
|
if (contextLoadedByClient) {
|
|
1284
1296
|
console.error(`[Prism] Auto-push skipped — client already loaded context`);
|
|
1285
1297
|
return;
|
|
1286
1298
|
}
|
|
1287
|
-
console.error(`[Prism] Auto-push triggered — model did not call
|
|
1299
|
+
console.error(`[Prism] Auto-push triggered — model did not call session_bootstrap within ${AUTOLOAD_PUSH_DELAY_MS / 1000}s`);
|
|
1288
1300
|
// Load and push context for each autoload project
|
|
1289
1301
|
try {
|
|
1290
1302
|
const storage = await getStorage();
|
|
@@ -111,14 +111,15 @@ export function requireContextLoaded(conversationId) {
|
|
|
111
111
|
return {
|
|
112
112
|
blocked: true,
|
|
113
113
|
error: "context_not_loaded: session expired (6 h TTL). Call " +
|
|
114
|
-
"session_load_context(project, conversation_id) again
|
|
114
|
+
"session_bootstrap(conversation_id) or session_load_context(project, conversation_id) again. " +
|
|
115
115
|
"(Enforced server-side — applies to every host.)",
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
if (!s || !s.contextLoaded) {
|
|
119
119
|
return {
|
|
120
120
|
blocked: true,
|
|
121
|
-
error: "context_not_loaded: call
|
|
121
|
+
error: "context_not_loaded: call session_bootstrap(conversation_id) or " +
|
|
122
|
+
"session_load_context(project, conversation_id) " +
|
|
122
123
|
"before this action. This project-scoped tool needs confirmed working context " +
|
|
123
124
|
"to act correctly. (Enforced server-side — applies to every host.)",
|
|
124
125
|
};
|
|
@@ -141,7 +142,8 @@ export function requireContextLoaded(conversationId) {
|
|
|
141
142
|
/** Best-effort telemetry from prism_infer. Never affects a safety decision. */
|
|
142
143
|
export function noteInferenceForSession(conversationId, info) {
|
|
143
144
|
// Only update sessions that already exist — don't create ghost stubs for
|
|
144
|
-
// conversations that never called session_load_context.
|
|
145
|
+
// conversations that never called session_bootstrap or session_load_context.
|
|
146
|
+
// Ghost stubs would
|
|
145
147
|
// accumulate in the LRU and crowd out legitimate registered sessions.
|
|
146
148
|
const s = sessions.get(conversationId);
|
|
147
149
|
if (!s)
|