exovault-mcp-server 1.2.0 → 1.3.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.
@@ -62,6 +62,12 @@ export declare class GatewayClient {
62
62
  includeArchived?: boolean;
63
63
  entity?: string;
64
64
  compact?: boolean;
65
+ decayHalfLife?: number;
66
+ diversity?: number;
67
+ searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
68
+ graphWeight?: number;
69
+ graphSeeds?: number;
70
+ graphMaxHops?: number;
65
71
  }): Promise<string>;
66
72
  readMemories(memoryIds: string[]): Promise<string>;
67
73
  updateMemory(params: {
package/dist/index.js CHANGED
@@ -605,6 +605,9 @@ async function main() {
605
605
  compact: s(z.boolean().optional().describe("Return truncated content previews (200 chars) instead of full content. Use read_memories for full content on specific IDs.")),
606
606
  decayHalfLife: s(z.number().int().min(1).max(365).optional().describe("Temporal decay half-life in days (default 30). Older memories score lower unless importance >= 4.")),
607
607
  diversity: s(z.number().min(0).max(1).optional().describe("MMR diversity balance 0-1 (default 0.7). Higher = more relevance, lower = more diversity.")),
608
+ graphWeight: s(z.number().min(0).max(1.5).optional().describe("RRF weight for graph signal (default 0.6). Set 0 to disable graph expansion.")),
609
+ graphSeeds: s(z.number().int().min(0).max(15).optional().describe("How many top results to use as graph expansion seeds (default 5)")),
610
+ graphMaxHops: s(z.number().int().min(1).max(2).optional().describe("Max hops for graph traversal (default 1)")),
608
611
  },
609
612
  }, auto.wrap(wrapToolHandler(async (args) => {
610
613
  const input = args;
@@ -11,4 +11,7 @@ export declare function searchMemories(ctx: McpContext, input: {
11
11
  decayHalfLife?: number;
12
12
  diversity?: number;
13
13
  searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
14
+ graphWeight?: number;
15
+ graphSeeds?: number;
16
+ graphMaxHops?: number;
14
17
  }): Promise<string>;
@@ -12,13 +12,27 @@ import { applyImportanceBoostBatch } from "./importance-boost.js";
12
12
  import { applyConfidenceDampenBatch } from "./confidence-dampen.js";
13
13
  const FALLBACK_CONTENT_PREVIEW_CHARS = 500;
14
14
  const COMPACT_CONTENT_CHARS = 200;
15
+ function buildSignalsMap(lists) {
16
+ const map = new Map();
17
+ for (const [signal, ids] of Object.entries(lists)) {
18
+ if (!ids)
19
+ continue;
20
+ for (const id of ids) {
21
+ const existing = map.get(id);
22
+ if (existing)
23
+ existing.push(signal);
24
+ else
25
+ map.set(id, [signal]);
26
+ }
27
+ }
28
+ return map;
29
+ }
15
30
  // RRF signal weights
16
31
  const WEIGHT_SEMANTIC = 1.0;
17
32
  const WEIGHT_BM25 = 0.9;
18
33
  const WEIGHT_BLIND_INDEX = 0.4;
19
- const WEIGHT_GRAPH = 0.6;
20
- // Graph expansion: how many initial results to expand, and relation weights
21
- const GRAPH_EXPAND_TOP_N = 5;
34
+ // WEIGHT_GRAPH and GRAPH_EXPAND_TOP_N are now configurable via input params (defaults applied inline)
35
+ // Graph relation weights for scoring neighbor relevance
22
36
  const GRAPH_RELATION_WEIGHTS = {
23
37
  refines: 1.0,
24
38
  derived_from: 0.9,
@@ -90,6 +104,7 @@ export async function searchMemories(ctx, input) {
90
104
  supersededById: m.superseded_by_id,
91
105
  entities: m.entities,
92
106
  updatedAt: m.updated_at,
107
+ signals: ["entity"],
93
108
  ...(attachmentLines.length > 0 ? { attachments: attachmentLines } : {}),
94
109
  };
95
110
  }));
@@ -187,30 +202,47 @@ export async function searchMemories(ctx, input) {
187
202
  await Promise.all([semanticPromise, blindPromise, bm25Promise]);
188
203
  // ── Signal 3: Graph expansion on top-N unique results from all signals ─
189
204
  const graphIds = [];
190
- const initialHits = new Set([...semanticIds, ...blindIds, ...bm25Ids]);
191
- const seedIds = [...initialHits].slice(0, GRAPH_EXPAND_TOP_N);
192
- if (seedIds.length > 0) {
193
- try {
194
- const graphResults = await Promise.all(seedIds.map((id) => getKnowledgeGraph(ctx.supabase, ctx.userId, "memory", id, 1, 10)));
195
- // Collect neighbor memory IDs ranked by relation weight
196
- const neighborScores = new Map();
197
- for (const neighbors of graphResults) {
198
- for (const n of neighbors) {
199
- if (n.node_type !== "memory" || initialHits.has(n.node_id))
200
- continue;
201
- const weight = GRAPH_RELATION_WEIGHTS[n.relation] ?? 0.5;
202
- const current = neighborScores.get(n.node_id) ?? 0;
203
- neighborScores.set(n.node_id, Math.max(current, weight));
205
+ const graphWeight = input.graphWeight ?? 0.6;
206
+ // Graph expansion — skip entirely when graphWeight=0
207
+ if (graphWeight > 0) {
208
+ const initialHits = new Set([...semanticIds, ...blindIds, ...bm25Ids]);
209
+ const graphSeedCount = input.graphSeeds ?? 5;
210
+ const seedIds = [...initialHits].slice(0, graphSeedCount);
211
+ if (seedIds.length > 0) {
212
+ try {
213
+ const graphMaxHops = input.graphMaxHops ?? 1;
214
+ const maxNodesPerSeed = graphMaxHops >= 2 ? 25 : 10;
215
+ const graphResults = await Promise.all(seedIds.map((id) => getKnowledgeGraph(ctx.supabase, ctx.userId, "memory", id, graphMaxHops, maxNodesPerSeed)));
216
+ // Collect neighbor memory IDs ranked by relation weight
217
+ const neighborScores = new Map();
218
+ for (const neighbors of graphResults) {
219
+ for (const n of neighbors) {
220
+ if (n.node_type !== "memory")
221
+ continue;
222
+ // Only exclude seed IDs — allow boosting of already-found results (aligned with gateway)
223
+ if (seedIds.includes(n.node_id))
224
+ continue;
225
+ const weight = GRAPH_RELATION_WEIGHTS[n.relation] ?? 0.5;
226
+ const current = neighborScores.get(n.node_id) ?? 0;
227
+ neighborScores.set(n.node_id, Math.max(current, weight));
228
+ }
204
229
  }
230
+ graphIds.push(...Array.from(neighborScores.entries())
231
+ .sort((a, b) => b[1] - a[1])
232
+ .map(([id]) => id));
233
+ }
234
+ catch (err) {
235
+ process.stderr.write(`[search-memories] Graph expansion error: ${err instanceof Error ? err.message : String(err)}\n`);
205
236
  }
206
- graphIds.push(...Array.from(neighborScores.entries())
207
- .sort((a, b) => b[1] - a[1])
208
- .map(([id]) => id));
209
- }
210
- catch (err) {
211
- process.stderr.write(`[search-memories] Graph expansion error: ${err instanceof Error ? err.message : String(err)}\n`);
212
237
  }
213
238
  }
239
+ // ── Signal attribution ──────────────────────────────────────────────
240
+ const signalsMap = buildSignalsMap({
241
+ semantic: semanticIds,
242
+ bm25: bm25Ids,
243
+ blind: blindIds,
244
+ graph: graphIds,
245
+ });
214
246
  // ── RRF Fusion + Temporal Decay ──────────────────────────────────────
215
247
  let rankedIds = [];
216
248
  let searchMode = "hybrid";
@@ -225,7 +257,7 @@ export async function searchMemories(ctx, input) {
225
257
  if (blindIds.length > 0)
226
258
  lists.push({ ids: blindIds, weight: WEIGHT_BLIND_INDEX });
227
259
  if (graphIds.length > 0)
228
- lists.push({ ids: graphIds, weight: WEIGHT_GRAPH });
260
+ lists.push({ ids: graphIds, weight: graphWeight });
229
261
  // Get scored candidates — fetch topK*2 for decay re-ranking headroom, with top-rank bonus
230
262
  const rrfScored = fuseWithRRFScored(lists, 60, true).slice(0, topK * 2);
231
263
  searchMode = effectiveSearchMode === "bm25" ? "bm25"
@@ -336,6 +368,7 @@ export async function searchMemories(ctx, input) {
336
368
  supersededById: m.superseded_by_id,
337
369
  entities: m.entities,
338
370
  updatedAt: m.updated_at,
371
+ signals: signalsMap.get(m.id) ?? [],
339
372
  ...(attachmentLines.length > 0 ? { attachments: attachmentLines } : {}),
340
373
  };
341
374
  }));
@@ -1,4 +1,13 @@
1
1
  import type { McpContext } from "../auth.js";
2
+ /**
3
+ * Direct-mode note search: keyword matching via blind-index pre-filter (when mekHex
4
+ * is available) or full client-side scan. Scores by weighted term frequency
5
+ * (title 3x, tags 2x, content 1x).
6
+ *
7
+ * NOTE: `searchMode`, `threshold`, and `diversity` are accepted for API compatibility
8
+ * but are IGNORED in direct mode. Hybrid/semantic/graph search modes and MMR
9
+ * diversity re-ranking require gateway mode (POST /api/agent/search-notes).
10
+ */
2
11
  export declare function searchNotes(ctx: McpContext, input: {
3
12
  query: string;
4
13
  topK?: number;
@@ -42,6 +42,15 @@ function scoreNote(terms, title, tags, content) {
42
42
  }
43
43
  const COMPACT_PREVIEW_CHARS = 200;
44
44
  const FULL_PREVIEW_CHARS = 500;
45
+ /**
46
+ * Direct-mode note search: keyword matching via blind-index pre-filter (when mekHex
47
+ * is available) or full client-side scan. Scores by weighted term frequency
48
+ * (title 3x, tags 2x, content 1x).
49
+ *
50
+ * NOTE: `searchMode`, `threshold`, and `diversity` are accepted for API compatibility
51
+ * but are IGNORED in direct mode. Hybrid/semantic/graph search modes and MMR
52
+ * diversity re-ranking require gateway mode (POST /api/agent/search-notes).
53
+ */
45
54
  export async function searchNotes(ctx, input) {
46
55
  const topK = input.topK ?? 10;
47
56
  const vaultId = input.vaultId;
@@ -1,4 +1,5 @@
1
1
  import type { McpContext } from "../auth.js";
2
+ import { GatewayClient } from "../gateway-client.js";
2
3
  interface SearchParams {
3
4
  query: string;
4
5
  topK?: number;
@@ -7,7 +8,6 @@ interface SearchParams {
7
8
  diversity?: number;
8
9
  vaultId?: string;
9
10
  includeContent?: boolean;
10
- compact?: boolean;
11
11
  scope?: "all" | "memories" | "notes";
12
12
  memoryType?: string;
13
13
  includeArchived?: boolean;
@@ -16,12 +16,13 @@ interface SearchParams {
16
16
  /**
17
17
  * Universal search tool — searches memories, notes, or both.
18
18
  *
19
- * In direct mode: best-effort calls searchMemories and/or searchNotes
20
- * independently and interleaves results (no cross-type MMR).
19
+ * In gateway mode: delegates to POST /api/agent/search which performs full
20
+ * cross-type MMR re-ranking server-side (hybrid semantic + keyword, all modes).
21
21
  *
22
- * Note: In gateway mode, the MCP server's `search` tool registration in index.ts
23
- * delegates to the gateway `/api/agent/search` endpoint which performs full
24
- * cross-type MMR re-ranking server-side. This function is only used in direct mode.
22
+ * In direct mode: best-effort calls searchMemories and/or searchNotes
23
+ * independently and interleaves results (no cross-type MMR). searchMode,
24
+ * threshold, and diversity are passed through but only partially honoured
25
+ * (notes fall back to keyword+blind-index only; see searchNotes).
25
26
  */
26
- export declare function search(ctx: McpContext, params: SearchParams): Promise<string>;
27
+ export declare function search(ctx: McpContext, params: SearchParams, gw?: GatewayClient): Promise<string>;
27
28
  export {};
@@ -3,14 +3,19 @@ import { searchNotes } from "./search-notes.js";
3
3
  /**
4
4
  * Universal search tool — searches memories, notes, or both.
5
5
  *
6
- * In direct mode: best-effort calls searchMemories and/or searchNotes
7
- * independently and interleaves results (no cross-type MMR).
6
+ * In gateway mode: delegates to POST /api/agent/search which performs full
7
+ * cross-type MMR re-ranking server-side (hybrid semantic + keyword, all modes).
8
8
  *
9
- * Note: In gateway mode, the MCP server's `search` tool registration in index.ts
10
- * delegates to the gateway `/api/agent/search` endpoint which performs full
11
- * cross-type MMR re-ranking server-side. This function is only used in direct mode.
9
+ * In direct mode: best-effort calls searchMemories and/or searchNotes
10
+ * independently and interleaves results (no cross-type MMR). searchMode,
11
+ * threshold, and diversity are passed through but only partially honoured
12
+ * (notes fall back to keyword+blind-index only; see searchNotes).
12
13
  */
13
- export async function search(ctx, params) {
14
+ export async function search(ctx, params, gw) {
15
+ // Gateway mode: full cross-type MMR re-ranking server-side
16
+ if (gw) {
17
+ return gw.search(params);
18
+ }
14
19
  const scope = params.scope ?? "all";
15
20
  const topK = params.topK ?? 10;
16
21
  const searchMemoriesParams = {
@@ -20,7 +25,6 @@ export async function search(ctx, params) {
20
25
  vaultId: params.vaultId,
21
26
  memoryType: params.memoryType,
22
27
  includeArchived: params.includeArchived,
23
- compact: params.compact,
24
28
  decayHalfLife: params.decayHalfLife,
25
29
  diversity: params.diversity,
26
30
  searchMode: params.searchMode,
@@ -33,7 +37,6 @@ export async function search(ctx, params) {
33
37
  diversity: params.diversity,
34
38
  vaultId: params.vaultId,
35
39
  includeContent: params.includeContent,
36
- compact: params.compact,
37
40
  };
38
41
  try {
39
42
  if (scope === "memories") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exovault-mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for ExoVault — read, search, and manage encrypted notes from Claude Code",
6
6
  "main": "dist/index.js",