prism-mcp-server 6.1.8 → 6.1.9

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 CHANGED
@@ -77,7 +77,7 @@ Add to your MCP client config (`claude_desktop_config.json`, `.cursor/mcp.json`,
77
77
  | Semantic vector search | ❌ | ✅ `GOOGLE_API_KEY` |
78
78
  | Morning Briefings | ❌ | ✅ `GOOGLE_API_KEY` |
79
79
  | Auto-compaction | ❌ | ✅ `GOOGLE_API_KEY` |
80
- | Web Scholar research | ❌ | ✅ `BRAVE_API_KEY` + `FIRECRAWL_API_KEY` |
80
+ | Web Scholar research | ❌ | ✅ `BRAVE_API_KEY` + `FIRECRAWL_API_KEY` (or `TAVILY_API_KEY`) |
81
81
  | VLM image captioning | ❌ | ✅ Provider key |
82
82
 
83
83
  > 🔑 The core Mind Palace works **100% offline** with zero API keys. Cloud keys unlock intelligence features. See [Environment Variables](#environment-variables).
@@ -382,7 +382,7 @@ Soft/hard delete (Art. 17), full export in JSON, Markdown, or Obsidian vault `.z
382
382
  ## 🆕 What's New
383
383
 
384
384
  ### v6.1 — Prism-Port, Cognitive Load & Semantic Search ✅
385
- > **Current stable release (v6.1.8).** Data sovereignty meets active memory intelligence.
385
+ > **Current stable release (v6.1.9).** Data sovereignty meets active memory intelligence.
386
386
 
387
387
  - 📦 **Prism-Port Vault Export** — New `vault` format for `session_export_memory`. Generates a `.zip` of interlinked Markdown files with YAML frontmatter, `[[Wikilinks]]`, and auto-generated `Keywords/` backlink indices. Drop into Obsidian or Logseq for instant knowledge graph.
388
388
  - 🏛️ **Dashboard Export Vault Button** — "🏛️ Export Vault" button in the Mind Palace UI exports the full Prism-Port vault ZIP directly from the browser. Both `/api/export` and `/api/export/vault` now use the unified `buildVaultDirectory` path — same rich format as the MCP tool.
@@ -392,6 +392,11 @@ Soft/hard delete (Art. 17), full export in JSON, Markdown, or Obsidian vault `.z
392
392
  - 📊 **Deep Purge Visualization** — A zero-overhead "Memory Density" analytic providing instant signal-to-noise ratio visibility (Graduated ideas vs raw concepts).
393
393
  - 🛡️ **Context-Boosted Search** — Biases semantic queries by intelligently interleaving your current project workspace.
394
394
 
395
+ #### v6.1.9 — Web Scholar Tavily Integration
396
+ - 🌐 **Tavily Core** — The Web Scholar now supports `@tavily/core` as an all-in-one search and extraction alternative to Brave+Firecrawl.
397
+ - 📦 **API Chunking Limits** — Advanced looping logic transparently works around `tavily.extract` 20-URL boundaries.
398
+ - 🛡️ **Network Resilience** — Handled promise rejections prevent data loss out-of-bounds due to chunk extraction or upstream network timeouts.
399
+
395
400
  #### v6.1.8 — Type Guard Hardening (Production Safety)
396
401
  - 🛡️ **Missing Guard Added** — `isSessionCompactLedgerArgs` was absent; an LLM passing `{threshold: "many"}` would reach the handler as a string. Added full validation for all four optional fields.
397
402
  - ✅ **Array Field Validation** — `isSessionSaveLedgerArgs` now guards `todos`, `files_changed`, and `decisions` with `Array.isArray` checks — prevents a hallucinated `{todos: "string"}` from bypassing the type system.
@@ -568,7 +573,8 @@ Requires `PRISM_ENABLE_HIVEMIND=true`.
568
573
  | Variable | Required | Description |
569
574
  |----------|----------|-------------|
570
575
  | `BRAVE_API_KEY` | No | Brave Search Pro API key |
571
- | `FIRECRAWL_API_KEY` | No | Firecrawl API key — required for Web Scholar |
576
+ | `FIRECRAWL_API_KEY` | No | Firecrawl API key — required for Web Scholar (unless using Tavily) |
577
+ | `TAVILY_API_KEY` | No | Tavily Search API key — alternative to Brave+Firecrawl for Web Scholar |
572
578
  | `PRISM_STORAGE` | No | `"local"` (default) or `"supabase"` — restart required |
573
579
  | `PRISM_ENABLE_HIVEMIND` | No | `"true"` to enable multi-agent tools — restart required |
574
580
  | `PRISM_INSTANCE` | No | Instance name for multi-server PID isolation |
package/dist/config.js CHANGED
@@ -145,11 +145,13 @@ export const PRISM_SCHEDULER_INTERVAL_MS = parseInt(process.env.PRISM_SCHEDULER_
145
145
  );
146
146
  // ─── v5.4: Autonomous Web Scholar ─────────────────────────────
147
147
  // Background LLM research pipeline powered by Brave Search + Firecrawl.
148
+ // Tavily can be used as an alternative when TAVILY_API_KEY is set.
148
149
  // Defaults are conservative to prevent runaway API costs.
149
150
  export const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;
151
+ export const TAVILY_API_KEY = process.env.TAVILY_API_KEY;
150
152
  export const PRISM_SCHOLAR_ENABLED = process.env.PRISM_SCHOLAR_ENABLED === "true"; // Opt-in
151
- if (PRISM_SCHOLAR_ENABLED && !FIRECRAWL_API_KEY) {
152
- console.error("Warning: FIRECRAWL_API_KEY environment variable is missing. Web Scholar will be unavailable.");
153
+ if (PRISM_SCHOLAR_ENABLED && !FIRECRAWL_API_KEY && !TAVILY_API_KEY) {
154
+ console.error("Warning: Neither FIRECRAWL_API_KEY nor TAVILY_API_KEY is set. Web Scholar will fall back to free search.");
153
155
  }
154
156
  export const PRISM_SCHOLAR_INTERVAL_MS = parseInt(process.env.PRISM_SCHOLAR_INTERVAL_MS || "0", 10 // Default manual-only
155
157
  );
@@ -1,9 +1,10 @@
1
- import { BRAVE_API_KEY, FIRECRAWL_API_KEY, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN, PRISM_USER_ID, PRISM_SCHOLAR_TOPICS, PRISM_ENABLE_HIVEMIND } from "../config.js";
1
+ import { BRAVE_API_KEY, FIRECRAWL_API_KEY, TAVILY_API_KEY, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN, PRISM_USER_ID, PRISM_SCHOLAR_TOPICS, PRISM_ENABLE_HIVEMIND } from "../config.js";
2
2
  import { getStorage } from "../storage/index.js";
3
3
  import { debugLog } from "../utils/logger.js";
4
4
  import { getLLMProvider } from "../utils/llm/factory.js";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { performWebSearchRaw } from "../utils/braveApi.js";
7
+ import { performTavilySearch, performTavilyExtract } from "../utils/tavilyApi.js";
7
8
  import { getTracer } from "../utils/telemetry.js";
8
9
  import { searchYahooFree, scrapeArticleLocal } from "./freeSearch.js";
9
10
  // ─── Hivemind Integration Helpers ────────────────────────────
@@ -129,7 +130,13 @@ export async function runWebScholar() {
129
130
  const tracer = getTracer();
130
131
  const span = tracer.startSpan("background.web_scholar");
131
132
  try {
132
- const useFreeFallback = !BRAVE_API_KEY || !FIRECRAWL_API_KEY;
133
+ // Pipeline priority:
134
+ // 1. Brave + Firecrawl (when both keys present)
135
+ // 2. Tavily Search + Extract (when TAVILY_API_KEY set but Brave/Firecrawl missing)
136
+ // 3. Yahoo + Readability free fallback (no paid keys)
137
+ const useBraveFirecrawl = !!(BRAVE_API_KEY && FIRECRAWL_API_KEY);
138
+ const useTavily = !useBraveFirecrawl && !!TAVILY_API_KEY;
139
+ const useFreeFallback = !useBraveFirecrawl && !useTavily;
133
140
  if (!PRISM_SCHOLAR_TOPICS || PRISM_SCHOLAR_TOPICS.length === 0) {
134
141
  debugLog("[WebScholar] Skipped: No topics configured in PRISM_SCHOLAR_TOPICS");
135
142
  span.setAttribute("scholar.skipped_reason", "no_topics");
@@ -148,16 +155,21 @@ export async function runWebScholar() {
148
155
  // 3. Search for articles
149
156
  await hivemindHeartbeat(`Searching for: ${topic}`);
150
157
  let urls = [];
151
- if (useFreeFallback) {
152
- debugLog("[WebScholar] API keys missing, falling back to Local Free Search (Yahoo + Readability)");
153
- const ddgResults = await searchYahooFree(topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
154
- urls = ddgResults.map(r => r.url).filter(Boolean);
155
- }
156
- else {
158
+ if (useBraveFirecrawl) {
157
159
  const braveResponse = await performWebSearchRaw(topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
158
160
  const braveData = JSON.parse(braveResponse);
159
161
  urls = (braveData.web?.results || []).map((r) => r.url).filter(Boolean);
160
162
  }
163
+ else if (useTavily) {
164
+ debugLog("[WebScholar] Using Tavily Search for URL discovery");
165
+ const tavilyResults = await performTavilySearch(TAVILY_API_KEY, topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
166
+ urls = tavilyResults.map(r => r.url).filter(Boolean);
167
+ }
168
+ else {
169
+ debugLog("[WebScholar] API keys missing, falling back to Local Free Search (Yahoo + Readability)");
170
+ const ddgResults = await searchYahooFree(topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
171
+ urls = ddgResults.map(r => r.url).filter(Boolean);
172
+ }
161
173
  if (urls.length === 0) {
162
174
  debugLog(`[WebScholar] No articles found for "${topic}"`);
163
175
  span.setAttribute("scholar.skipped_reason", "no_search_results");
@@ -168,44 +180,62 @@ export async function runWebScholar() {
168
180
  // 4. Scrape each URL
169
181
  await hivemindHeartbeat(`Scraping ${urls.length} articles on: ${topic}`);
170
182
  const scrapedTexts = [];
171
- for (const url of urls) {
172
- if (useFreeFallback) {
173
- try {
174
- debugLog(`[WebScholar] Scraping local fallback: ${url}`);
175
- const article = await scrapeArticleLocal(url);
176
- const trimmed = article.content.slice(0, 15_000);
177
- scrapedTexts.push(`Source: ${url}\nTitle: ${article.title}\n\n${trimmed}\n\n---\n`);
178
- }
179
- catch (err) {
180
- console.error(`[WebScholar] Failed to locally scrape ${url}:`, err);
183
+ if (useTavily) {
184
+ // Tavily Extract: batch all URLs at once (up to 20)
185
+ try {
186
+ debugLog(`[WebScholar] Extracting ${urls.length} URLs via Tavily Extract`);
187
+ const extracted = await performTavilyExtract(TAVILY_API_KEY, urls);
188
+ for (const item of extracted) {
189
+ if (item.rawContent) {
190
+ const trimmed = item.rawContent.slice(0, 15_000);
191
+ scrapedTexts.push(`Source: ${item.url}\n\n${trimmed}\n\n---\n`);
192
+ }
181
193
  }
182
194
  }
183
- else {
184
- try {
185
- debugLog(`[WebScholar] Scraping Firecrawl: ${url}`);
186
- const scrapeRes = await fetch("https://api.firecrawl.dev/v1/scrape", {
187
- method: "POST",
188
- headers: {
189
- "Content-Type": "application/json",
190
- "Authorization": `Bearer ${FIRECRAWL_API_KEY}`
191
- },
192
- body: JSON.stringify({
193
- url,
194
- formats: ["markdown"],
195
- })
196
- });
197
- if (!scrapeRes.ok) {
198
- console.error(`[WebScholar] Firecrawl failed for ${url}: ${scrapeRes.status}`);
199
- continue;
195
+ catch (err) {
196
+ console.error("[WebScholar] Tavily Extract failed:", err);
197
+ }
198
+ }
199
+ else {
200
+ for (const url of urls) {
201
+ if (useFreeFallback) {
202
+ try {
203
+ debugLog(`[WebScholar] Scraping local fallback: ${url}`);
204
+ const article = await scrapeArticleLocal(url);
205
+ const trimmed = article.content.slice(0, 15_000);
206
+ scrapedTexts.push(`Source: ${url}\nTitle: ${article.title}\n\n${trimmed}\n\n---\n`);
200
207
  }
201
- const result = (await scrapeRes.json());
202
- if (result.success && result.data?.markdown) {
203
- const trimmed = result.data.markdown.slice(0, 15_000);
204
- scrapedTexts.push(`Source: ${url}\n\n${trimmed}\n\n---\n`);
208
+ catch (err) {
209
+ console.error(`[WebScholar] Failed to locally scrape ${url}:`, err);
205
210
  }
206
211
  }
207
- catch (err) {
208
- console.error(`[WebScholar] Failed to scrape ${url}:`, err);
212
+ else {
213
+ try {
214
+ debugLog(`[WebScholar] Scraping Firecrawl: ${url}`);
215
+ const scrapeRes = await fetch("https://api.firecrawl.dev/v1/scrape", {
216
+ method: "POST",
217
+ headers: {
218
+ "Content-Type": "application/json",
219
+ "Authorization": `Bearer ${FIRECRAWL_API_KEY}`
220
+ },
221
+ body: JSON.stringify({
222
+ url,
223
+ formats: ["markdown"],
224
+ })
225
+ });
226
+ if (!scrapeRes.ok) {
227
+ console.error(`[WebScholar] Firecrawl failed for ${url}: ${scrapeRes.status}`);
228
+ continue;
229
+ }
230
+ const result = (await scrapeRes.json());
231
+ if (result.success && result.data?.markdown) {
232
+ const trimmed = result.data.markdown.slice(0, 15_000);
233
+ scrapedTexts.push(`Source: ${url}\n\n${trimmed}\n\n---\n`);
234
+ }
235
+ }
236
+ catch (err) {
237
+ console.error(`[WebScholar] Failed to scrape ${url}:`, err);
238
+ }
209
239
  }
210
240
  }
211
241
  }
@@ -133,8 +133,16 @@ export const SESSION_LOAD_CONTEXT_TOOL = {
133
133
  type: "integer",
134
134
  description: "Maximum token budget for context response. Uses 1 token ≈ 4 chars heuristic. When set, the response is truncated to fit within the budget. Default: unlimited.",
135
135
  },
136
+ toolAction: {
137
+ type: "string",
138
+ description: "Brief 2-5 word summary of what this tool is doing. Capitalize like a sentence.",
139
+ },
140
+ toolSummary: {
141
+ type: "string",
142
+ description: "Brief 2-5 word noun phrase describing what this tool call is about.",
143
+ },
136
144
  },
137
- required: ["project"],
145
+ required: ["project", "toolAction", "toolSummary"],
138
146
  },
139
147
  };
140
148
  // ─── Knowledge Search ─────────────────────────────────────────
@@ -181,6 +189,7 @@ export const KNOWLEDGE_SEARCH_TOOL = {
181
189
  "latency breakdown, and scoring metadata for explainability. Default: false.",
182
190
  },
183
191
  },
192
+ required: ["query"],
184
193
  },
185
194
  };
186
195
  // ─── Knowledge Forget ─────────────────────────────────────────
@@ -421,9 +430,9 @@ export function isKnowledgeSearchArgs(args) {
421
430
  if (typeof args !== "object" || args === null)
422
431
  return false;
423
432
  const a = args;
424
- if (a.project !== undefined && typeof a.project !== "string")
433
+ if (typeof a.query !== "string")
425
434
  return false;
426
- if (a.query !== undefined && typeof a.query !== "string")
435
+ if (a.project !== undefined && typeof a.project !== "string")
427
436
  return false;
428
437
  if (a.category !== undefined && typeof a.category !== "string")
429
438
  return false;
@@ -444,12 +453,12 @@ export function isSessionSaveLedgerArgs(args) {
444
453
  return false;
445
454
  if (typeof a.summary !== "string")
446
455
  return false;
447
- // Optional array fields — guard against LLM passing a string instead of string[]
448
- if (a.todos !== undefined && !Array.isArray(a.todos))
456
+ // Optional array fields — guard against LLM passing a string instead of string[] and check elements
457
+ if (a.todos !== undefined && (!Array.isArray(a.todos) || !a.todos.every(t => typeof t === "string")))
449
458
  return false;
450
- if (a.files_changed !== undefined && !Array.isArray(a.files_changed))
459
+ if (a.files_changed !== undefined && (!Array.isArray(a.files_changed) || !a.files_changed.every(t => typeof t === "string")))
451
460
  return false;
452
- if (a.decisions !== undefined && !Array.isArray(a.decisions))
461
+ if (a.decisions !== undefined && (!Array.isArray(a.decisions) || !a.decisions.every(t => typeof t === "string")))
453
462
  return false;
454
463
  if (a.role !== undefined && typeof a.role !== "string")
455
464
  return false;
@@ -466,7 +475,7 @@ export function isSessionSaveHandoffArgs(args) {
466
475
  return false;
467
476
  if (a.expected_version !== undefined && typeof a.expected_version !== "number")
468
477
  return false;
469
- if (a.open_todos !== undefined && !Array.isArray(a.open_todos))
478
+ if (a.open_todos !== undefined && (!Array.isArray(a.open_todos) || !a.open_todos.every(t => typeof t === "string")))
470
479
  return false;
471
480
  if (a.active_branch !== undefined && typeof a.active_branch !== "string")
472
481
  return false;
@@ -516,10 +525,12 @@ export function isBackfillEmbeddingsArgs(args) {
516
525
  return true;
517
526
  }
518
527
  export function isBackfillLinksArgs(args) {
519
- return (typeof args === "object" &&
520
- args !== null &&
521
- "project" in args &&
522
- typeof args.project === "string");
528
+ if (typeof args !== "object" || args === null)
529
+ return false;
530
+ const a = args;
531
+ if (typeof a.project !== "string")
532
+ return false;
533
+ return true;
523
534
  }
524
535
  export function isSessionLoadContextArgs(args) {
525
536
  if (typeof args !== "object" || args === null)
@@ -533,6 +544,10 @@ export function isSessionLoadContextArgs(args) {
533
544
  return false;
534
545
  if (a.max_tokens !== undefined && typeof a.max_tokens !== "number")
535
546
  return false;
547
+ if (a.toolAction !== undefined && typeof a.toolAction !== "string")
548
+ return false;
549
+ if (a.toolSummary !== undefined && typeof a.toolSummary !== "string")
550
+ return false;
536
551
  return true;
537
552
  }
538
553
  // ─── v2.0: Time Travel Tool Definitions ──────────────────────
@@ -659,12 +674,14 @@ export function isSessionSaveImageArgs(args) {
659
674
  return true;
660
675
  }
661
676
  export function isSessionViewImageArgs(args) {
662
- return (typeof args === "object" &&
663
- args !== null &&
664
- "project" in args &&
665
- typeof args.project === "string" &&
666
- "image_id" in args &&
667
- typeof args.image_id === "string");
677
+ if (typeof args !== "object" || args === null)
678
+ return false;
679
+ const a = args;
680
+ if (typeof a.project !== "string")
681
+ return false;
682
+ if (typeof a.image_id !== "string")
683
+ return false;
684
+ return true;
668
685
  }
669
686
  // ─── v2.2.0: Health Check (fsck) Tool Definition ─────────────
670
687
  /**
@@ -675,12 +692,12 @@ export function isSessionViewImageArgs(args) {
675
692
  export const SESSION_HEALTH_CHECK_TOOL = {
676
693
  name: "session_health_check",
677
694
  description: "Run integrity checks on the agent's memory (like fsck for filesystems). " +
678
- "Scans for missing embeddings, duplicate entries, orphaned handoffs, and stale rollups.\\n\\n" +
679
- "Checks performed:\\n" +
680
- "1. **Missing embeddings** — entries that can't be found via semantic search\\n" +
681
- "2. **Duplicate entries** — near-identical summaries wasting context tokens\\n" +
682
- "3. **Orphaned handoffs** — handoff state with no backing ledger entries\\n" +
683
- "4. **Stale rollups** — compaction artifacts with no archived originals\\n\\n" +
695
+ "Scans for missing embeddings, duplicate entries, orphaned handoffs, and stale rollups.\n\n" +
696
+ "Checks performed:\n" +
697
+ "1. **Missing embeddings** — entries that can't be found via semantic search\n" +
698
+ "2. **Duplicate entries** — near-identical summaries wasting context tokens\n" +
699
+ "3. **Orphaned handoffs** — handoff state with no backing ledger entries\n" +
700
+ "4. **Stale rollups** — compaction artifacts with no archived originals\n\n" +
684
701
  "Use auto_fix=true to automatically repair missing embeddings and clean up orphans.",
685
702
  inputSchema: {
686
703
  type: "object",
@@ -864,12 +881,14 @@ export const KNOWLEDGE_SET_RETENTION_TOOL = {
864
881
  },
865
882
  };
866
883
  export function isKnowledgeSetRetentionArgs(args) {
867
- return (typeof args === "object" &&
868
- args !== null &&
869
- "project" in args &&
870
- typeof args.project === "string" &&
871
- "ttl_days" in args &&
872
- typeof args.ttl_days === "number");
884
+ if (typeof args !== "object" || args === null)
885
+ return false;
886
+ const a = args;
887
+ if (typeof a.project !== "string")
888
+ return false;
889
+ if (typeof a.ttl_days !== "number")
890
+ return false;
891
+ return true;
873
892
  }
874
893
  // ─── v4.0: Active Behavioral Memory Tools ────────────────────
875
894
  export const SESSION_SAVE_EXPERIENCE_TOOL = {
@@ -929,7 +948,11 @@ export function isSessionSaveExperienceArgs(args) {
929
948
  const a = args;
930
949
  if (typeof a.project !== "string")
931
950
  return false;
932
- if (typeof a.event_type !== "string")
951
+ if (typeof a.event_type !== "string" ||
952
+ (a.event_type !== "correction" &&
953
+ a.event_type !== "success" &&
954
+ a.event_type !== "failure" &&
955
+ a.event_type !== "learning"))
933
956
  return false;
934
957
  if (typeof a.context !== "string")
935
958
  return false;
@@ -977,10 +1000,12 @@ export const KNOWLEDGE_DOWNVOTE_TOOL = {
977
1000
  },
978
1001
  };
979
1002
  export function isKnowledgeVoteArgs(args) {
980
- return (typeof args === "object" &&
981
- args !== null &&
982
- "id" in args &&
983
- typeof args.id === "string");
1003
+ if (typeof args !== "object" || args === null)
1004
+ return false;
1005
+ const a = args;
1006
+ if (typeof a.id !== "string")
1007
+ return false;
1008
+ return true;
984
1009
  }
985
1010
  // ─── v4.2: Knowledge Sync Rules Tool ─────────────────────────
986
1011
  export const KNOWLEDGE_SYNC_RULES_TOOL = {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Tavily API Client
3
+ *
4
+ * This module provides Tavily Search and Extract helpers for the Web Scholar
5
+ * pipeline. It serves as an additive alternative to Brave Search + Firecrawl
6
+ * when TAVILY_API_KEY is set.
7
+ *
8
+ * 1. performTavilySearch — Web search returning URLs (mirrors Brave web search)
9
+ * 2. performTavilyExtract — URL content extraction returning markdown (mirrors Firecrawl scrape)
10
+ */
11
+ import { tavily } from "@tavily/core";
12
+ function getClient(apiKey) {
13
+ return tavily({ apiKey });
14
+ }
15
+ /**
16
+ * Searches the web via Tavily and returns an array of result objects.
17
+ */
18
+ export async function performTavilySearch(apiKey, query, maxResults = 10) {
19
+ try {
20
+ const client = getClient(apiKey);
21
+ const response = await client.search(query, {
22
+ maxResults,
23
+ searchDepth: "advanced",
24
+ topic: "general",
25
+ });
26
+ return (response.results || []).map((r) => ({
27
+ title: r.title || "",
28
+ url: r.url || "",
29
+ content: r.content || "",
30
+ score: r.score ?? 0,
31
+ }));
32
+ }
33
+ catch (error) {
34
+ console.error(`[Tavily Search] Error performing search for query "${query}":`, error);
35
+ return [];
36
+ }
37
+ }
38
+ /**
39
+ * Extracts article content from URLs via Tavily Extract.
40
+ * Returns markdown content for each successfully extracted URL.
41
+ */
42
+ export async function performTavilyExtract(apiKey, urls) {
43
+ if (urls.length === 0)
44
+ return [];
45
+ const client = getClient(apiKey);
46
+ const allResults = [];
47
+ // Tavily extract accepts up to 20 URLs at once
48
+ for (let i = 0; i < urls.length; i += 20) {
49
+ const batch = urls.slice(i, i + 20);
50
+ try {
51
+ const response = await client.extract(batch, {
52
+ extractDepth: "basic",
53
+ });
54
+ // Optionally log failed URLs from this batch
55
+ if (response.failedResults && response.failedResults.length > 0) {
56
+ console.warn(`[Tavily Extract] Failed to extract ${response.failedResults.length} URLs in this batch.`, response.failedResults);
57
+ }
58
+ const mapped = (response.results || []).map((r) => ({
59
+ url: r.url || "",
60
+ rawContent: r.rawContent || "",
61
+ }));
62
+ allResults.push(...mapped);
63
+ }
64
+ catch (error) {
65
+ // Log the error but continue to the next batch to prevent total data loss
66
+ console.error(`[Tavily Extract] Error extracting batch ${i} to ${Math.min(i + 20, urls.length)}:`, error);
67
+ }
68
+ }
69
+ return allResults;
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "6.1.8",
3
+ "version": "6.1.9",
4
4
  "mcpName": "io.github.dcostenco/prism-mcp",
5
5
  "description": "The Mind Palace for AI Agents — persistent memory (SQLite/Supabase), behavioral learning & IDE rules sync, multimodal VLM image captioning, pluggable LLM providers (OpenAI/Anthropic/Gemini/Ollama), OpenTelemetry distributed tracing, GDPR export, multi-agent Hivemind sync, time travel, visual Mind Palace dashboard. Zero-config local mode.",
6
6
  "module": "index.ts",
@@ -87,6 +87,7 @@
87
87
  },
88
88
  "dependencies": {
89
89
  "@anthropic-ai/sdk": "^0.80.0",
90
+ "@tavily/core": "^0.6.0",
90
91
  "@google-cloud/discoveryengine": "^2.5.3",
91
92
  "@google/generative-ai": "^0.24.1",
92
93
  "@libsql/client": "^0.17.2",