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.
- package/dist/utils/braveApi.js +52 -0
- package/dist/utils/synaluxSearch.js +195 -0
- package/package.json +1 -1
package/dist/utils/braveApi.js
CHANGED
|
@@ -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.
|
|
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",
|