prism-mcp-server 17.0.0 → 17.0.1

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.
@@ -29,8 +29,20 @@
29
29
  * The Brave Answers endpoint uses a separate BRAVE_ANSWERS_API_KEY via Bearer token.
30
30
  */
31
31
  import { BRAVE_API_KEY, BRAVE_ANSWERS_API_KEY } from "../config.js";
32
+ import { SYNALUX_SEARCH_AVAILABLE, synaluxWebSearch, synaluxWebSearchRaw, synaluxLocalSearch, synaluxLocalSearchRaw, synaluxBraveAnswers, } from "./synaluxSearch.js";
33
+ import { debugLog } from "./logger.js";
32
34
  // Brave Answers API call (AI Grounding/OpenAI-compatible)
33
35
  export async function performBraveAnswers(query, model = "brave") {
36
+ // Route through Synalux portal when available
37
+ if (SYNALUX_SEARCH_AVAILABLE) {
38
+ try {
39
+ return await synaluxBraveAnswers(query, model);
40
+ }
41
+ catch (err) {
42
+ debugLog(`[braveApi] Synalux answers failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
43
+ // Fall through to direct Brave API
44
+ }
45
+ }
34
46
  if (!BRAVE_ANSWERS_API_KEY) {
35
47
  throw new Error("BRAVE_ANSWERS_API_KEY is not configured");
36
48
  }
@@ -62,6 +74,16 @@ export async function performBraveAnswers(query, model = "brave") {
62
74
  }
63
75
  // Raw web search API call
64
76
  export async function performWebSearchRaw(query, count = 10, offset = 0) {
77
+ // Route through Synalux portal when available (offset=0 only — portal doesn't support offset)
78
+ if (SYNALUX_SEARCH_AVAILABLE && offset === 0) {
79
+ try {
80
+ return await synaluxWebSearchRaw(query, count);
81
+ }
82
+ catch (err) {
83
+ debugLog(`[braveApi] Synalux search failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
84
+ // Fall through to direct Brave API
85
+ }
86
+ }
65
87
  const url = new URL("https://api.search.brave.com/res/v1/web/search");
66
88
  url.searchParams.set("q", query);
67
89
  url.searchParams.set("count", Math.min(count, 20).toString()); // API limit
@@ -81,6 +103,16 @@ export async function performWebSearchRaw(query, count = 10, offset = 0) {
81
103
  }
82
104
  // Web search API call
83
105
  export async function performWebSearch(query, count = 10, offset = 0) {
106
+ // Route through Synalux portal when available (offset=0 only — portal doesn't support offset)
107
+ if (SYNALUX_SEARCH_AVAILABLE && offset === 0) {
108
+ try {
109
+ return await synaluxWebSearch(query, count);
110
+ }
111
+ catch (err) {
112
+ debugLog(`[braveApi] Synalux search failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
113
+ // Fall through to direct Brave API
114
+ }
115
+ }
84
116
  const textData = await performWebSearchRaw(query, count, offset);
85
117
  const data = JSON.parse(textData);
86
118
  // Extract just web results
@@ -136,6 +168,16 @@ function chunkArray(arr, size) {
136
168
  }
137
169
  // Raw local search API call with poi/details payload
138
170
  export async function performLocalSearchRaw(query, count = 5) {
171
+ // Route through Synalux portal when available
172
+ if (SYNALUX_SEARCH_AVAILABLE) {
173
+ try {
174
+ return await synaluxLocalSearchRaw(query, count);
175
+ }
176
+ catch (err) {
177
+ debugLog(`[braveApi] Synalux local search raw failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
178
+ // Fall through to direct Brave API
179
+ }
180
+ }
139
181
  // Initial search to get location IDs
140
182
  const webUrl = new URL("https://api.search.brave.com/res/v1/web/search");
141
183
  webUrl.searchParams.set("q", query);
@@ -190,6 +232,16 @@ export async function performLocalSearchRaw(query, count = 5) {
190
232
  }
191
233
  // Local search API call with poi details
192
234
  export async function performLocalSearch(query, count = 5) {
235
+ // Route through Synalux portal when available
236
+ if (SYNALUX_SEARCH_AVAILABLE) {
237
+ try {
238
+ return await synaluxLocalSearch(query, count);
239
+ }
240
+ catch (err) {
241
+ debugLog(`[braveApi] Synalux local search failed, falling back to Brave: ${err instanceof Error ? err.message : String(err)}`);
242
+ // Fall through to direct Brave API
243
+ }
244
+ }
193
245
  const rawData = await performLocalSearchRaw(query, count);
194
246
  const parsed = JSON.parse(rawData);
195
247
  if (parsed.source === "web_fallback") {
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Synalux Portal Search & Scrape Client
3
+ * ─────────────────────────────────────────────────────────────
4
+ * Routes web search and scrape calls through the Synalux portal
5
+ * so API keys (Brave, Firecrawl, etc.) live server-side. The
6
+ * portal endpoints:
7
+ *
8
+ * POST /api/v1/prism/search — { query, limit? }
9
+ * returns { status, results: [{title, url, description}], source }
10
+ *
11
+ * POST /api/v1/prism/scrape — { url, formats?, onlyMainContent?, waitFor? }
12
+ * returns { status, content }
13
+ *
14
+ * Auth uses the shared JWT exchange from synaluxJwt.ts (same
15
+ * refresh-token dance as SynaluxStorage, but without requiring
16
+ * the full storage class). Falls back gracefully: callers check
17
+ * SYNALUX_SEARCH_AVAILABLE before calling.
18
+ */
19
+ import { debugLog } from "./logger.js";
20
+ import { getSynaluxJwt, invalidateSynaluxJwt } from "./synaluxJwt.js";
21
+ import { PRISM_SYNALUX_BASE_URL, SYNALUX_CONFIGURED, } from "../config.js";
22
+ // ─── Public availability flag ────────────────────────────────
23
+ /** True when Synalux portal credentials are configured. */
24
+ export const SYNALUX_SEARCH_AVAILABLE = SYNALUX_CONFIGURED;
25
+ // ─── Internal helpers ────────────────────────────────────────
26
+ /**
27
+ * POST to a portal endpoint with JWT auth. Retries once on 401
28
+ * (JWT may have just expired). Throws on network or HTTP errors.
29
+ */
30
+ async function portalPost(path, body, timeoutMs = 15_000) {
31
+ const baseUrl = PRISM_SYNALUX_BASE_URL.replace(/\/+$/, "");
32
+ const url = `${baseUrl}${path}`;
33
+ const send = async (jwt) => {
34
+ return fetch(url, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ "Authorization": `Bearer ${jwt}`,
39
+ "X-Prism-Client": "prism-mcp-search",
40
+ },
41
+ body: JSON.stringify(body),
42
+ signal: AbortSignal.timeout(timeoutMs),
43
+ });
44
+ };
45
+ let jwt = await getSynaluxJwt();
46
+ if (!jwt) {
47
+ throw new Error("[synaluxSearch] JWT exchange failed — no token available");
48
+ }
49
+ let res = await send(jwt);
50
+ // Retry once on 401 (stale JWT)
51
+ if (res.status === 401) {
52
+ debugLog("[synaluxSearch] 401 on first attempt, re-exchanging JWT");
53
+ invalidateSynaluxJwt();
54
+ jwt = await getSynaluxJwt();
55
+ if (!jwt) {
56
+ throw new Error("[synaluxSearch] JWT re-exchange failed after 401");
57
+ }
58
+ res = await send(jwt);
59
+ }
60
+ if (!res.ok) {
61
+ const text = await res.text().catch(() => "(no body)");
62
+ throw new Error(`[synaluxSearch] ${path} HTTP ${res.status}: ${text}`);
63
+ }
64
+ return (await res.json());
65
+ }
66
+ // ─── Public API ──────────────────────────────────────────────
67
+ /**
68
+ * Web search via Synalux portal. Returns formatted text matching
69
+ * the shape of performWebSearch() in braveApi.ts.
70
+ */
71
+ export async function synaluxWebSearch(query, count = 10) {
72
+ debugLog(`[synaluxSearch] web search: q="${query}", limit=${count}`);
73
+ const data = await portalPost("/api/v1/prism/search", {
74
+ query,
75
+ limit: Math.min(count, 20),
76
+ });
77
+ if (data.status === "error") {
78
+ throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
79
+ }
80
+ const results = (data.results || []).map((r) => ({
81
+ title: r.title || "",
82
+ description: r.description || "",
83
+ url: r.url || "",
84
+ }));
85
+ debugLog(`[synaluxSearch] got ${results.length} results (source=${data.source || "portal"})`);
86
+ return results
87
+ .map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`)
88
+ .join("\n\n");
89
+ }
90
+ /**
91
+ * Web search via Synalux portal — returns raw JSON string.
92
+ * Used by code-mode handlers that pass raw data to the QuickJS sandbox.
93
+ */
94
+ export async function synaluxWebSearchRaw(query, count = 10) {
95
+ debugLog(`[synaluxSearch] web search raw: q="${query}", limit=${count}`);
96
+ const data = await portalPost("/api/v1/prism/search", {
97
+ query,
98
+ limit: Math.min(count, 20),
99
+ });
100
+ if (data.status === "error") {
101
+ throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
102
+ }
103
+ // Re-shape into the Brave-compatible format that code-mode handlers expect
104
+ const braveCompatible = {
105
+ web: {
106
+ results: (data.results || []).map((r) => ({
107
+ title: r.title || "",
108
+ description: r.description || "",
109
+ url: r.url || "",
110
+ })),
111
+ },
112
+ };
113
+ debugLog(`[synaluxSearch] raw: ${braveCompatible.web.results.length} results`);
114
+ return JSON.stringify(braveCompatible);
115
+ }
116
+ /**
117
+ * Local/POI search via Synalux portal.
118
+ * Returns formatted text matching performLocalSearch() shape.
119
+ */
120
+ export async function synaluxLocalSearch(query, count = 5) {
121
+ debugLog(`[synaluxSearch] local search: q="${query}", count=${count}`);
122
+ const data = await portalPost("/api/v1/prism/local-search", {
123
+ query,
124
+ count: Math.min(count, 20),
125
+ });
126
+ if (data.status === "error") {
127
+ throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
128
+ }
129
+ const results = data.results || [];
130
+ debugLog(`[synaluxSearch] local: got ${results.length} results`);
131
+ return results
132
+ .map((r) => `Name: ${r.name || "N/A"}\nAddress: ${r.address || "N/A"}\nPhone: ${r.phone || "N/A"}\nRating: ${r.rating || "N/A"}\nHours: ${r.hours || "N/A"}\nDescription: ${r.description || "No description available"}`)
133
+ .join("\n---\n");
134
+ }
135
+ /**
136
+ * Local/POI search raw — returns JSON string for code-mode sandbox.
137
+ */
138
+ export async function synaluxLocalSearchRaw(query, count = 5) {
139
+ debugLog(`[synaluxSearch] local search raw: q="${query}", count=${count}`);
140
+ const data = await portalPost("/api/v1/prism/local-search", {
141
+ query,
142
+ count: Math.min(count, 20),
143
+ });
144
+ if (data.status === "error") {
145
+ throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
146
+ }
147
+ const results = data.results || [];
148
+ debugLog(`[synaluxSearch] local raw: ${results.length} results`);
149
+ // Build envelope compatible with code-mode sandbox expectations
150
+ const envelope = {
151
+ source: "local",
152
+ query,
153
+ count,
154
+ poisData: { results },
155
+ descriptionsData: {
156
+ descriptions: Object.fromEntries(results.map((r, i) => [String(i), r.description || ""])),
157
+ },
158
+ };
159
+ return JSON.stringify(envelope);
160
+ }
161
+ /**
162
+ * AI-grounded answers via Synalux portal.
163
+ */
164
+ export async function synaluxBraveAnswers(query, model) {
165
+ debugLog(`[synaluxSearch] answers: q="${query}", model=${model || "default"}`);
166
+ const body = { query };
167
+ if (model)
168
+ body.model = model;
169
+ const data = await portalPost("/api/v1/prism/answers", body);
170
+ if (data.status === "error") {
171
+ throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
172
+ }
173
+ if (!data.answer) {
174
+ throw new Error("[synaluxSearch] answers endpoint returned empty answer");
175
+ }
176
+ return data.answer;
177
+ }
178
+ /**
179
+ * Scrape a URL via Synalux portal. Returns the extracted content string.
180
+ */
181
+ export async function synaluxScrape(url, options) {
182
+ debugLog(`[synaluxSearch] scrape: url="${url}"`);
183
+ const body = { url };
184
+ if (options?.formats)
185
+ body.formats = options.formats;
186
+ if (options?.onlyMainContent !== undefined)
187
+ body.onlyMainContent = options.onlyMainContent;
188
+ if (options?.waitFor !== undefined)
189
+ body.waitFor = options.waitFor;
190
+ const data = await portalPost("/api/v1/prism/scrape", body);
191
+ if (data.status === "error") {
192
+ throw new Error(`[synaluxSearch] scrape error: ${data.error || "unknown"}`);
193
+ }
194
+ return data.content || "";
195
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "17.0.0",
3
+ "version": "17.0.1",
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, 54 Agent Skills, 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:7b / 14b open-weights LLM fleet.",
6
6
  "module": "index.ts",