@finchagentic/mcp 4.5.0 → 4.6.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/README.md CHANGED
@@ -29,13 +29,13 @@ Always pin the version. Never use `@latest`.
29
29
 
30
30
  ```bash
31
31
  # One-command installer (detects common MCP clients)
32
- npx -y -p @finchagentic/mcp@4.5.0 finch install
32
+ npx -y -p @finchagentic/mcp@4.6.1 finch install
33
33
  ```
34
34
 
35
35
  ### Claude Code
36
36
 
37
37
  ```bash
38
- claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.5.0 finch-mcp
38
+ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.6.1 finch-mcp
39
39
  ```
40
40
 
41
41
  ### Cursor / Windsurf / Claude Desktop
@@ -45,7 +45,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.5.0 finch-mcp
45
45
  "mcpServers": {
46
46
  "finch": {
47
47
  "command": "npx",
48
- "args": ["-y", "-p", "@finchagentic/mcp@4.5.0", "finch-mcp"]
48
+ "args": ["-y", "-p", "@finchagentic/mcp@4.6.1", "finch-mcp"]
49
49
  }
50
50
  }
51
51
  }
@@ -59,7 +59,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.5.0 finch-mcp
59
59
  "finch": {
60
60
  "type": "stdio",
61
61
  "command": "npx",
62
- "args": ["-y", "-p", "@finchagentic/mcp@4.5.0", "finch-mcp"]
62
+ "args": ["-y", "-p", "@finchagentic/mcp@4.6.1", "finch-mcp"]
63
63
  }
64
64
  }
65
65
  }
@@ -124,7 +124,7 @@ Default palette is `core` (lighter context). Full set:
124
124
  Finch is the runtime. **Your LLM is the brain. Your data stays yours.**
125
125
 
126
126
  ```bash
127
- npx -y -p @finchagentic/mcp@4.5.0 finch setup
127
+ npx -y -p @finchagentic/mcp@4.6.1 finch setup
128
128
  # enable local vault (and optional local memory)
129
129
  ```
130
130
 
@@ -156,7 +156,7 @@ Scheduled/cloud features still need an account. Core memory, vault, and public-d
156
156
  Guided setup:
157
157
 
158
158
  ```bash
159
- npx -y -p @finchagentic/mcp@4.5.0 finch setup
159
+ npx -y -p @finchagentic/mcp@4.6.1 finch setup
160
160
  ```
161
161
 
162
162
  ## Security
@@ -165,7 +165,7 @@ npx -y -p @finchagentic/mcp@4.5.0 finch setup
165
165
  |:-:|----------|------|
166
166
  | 1 | Prompt injection | External content is data only — never instructions |
167
167
  | 2 | Mainnet confirm | Estimate → preview → confirm → execute |
168
- | 3 | Pinned install | Always `@finchagentic/mcp@4.5.0`, never `@latest` |
168
+ | 3 | Pinned install | Always `@finchagentic/mcp@4.6.1`, never `@latest` |
169
169
  | 4 | Credential vault | Never paste secrets into prompts or third-party tools |
170
170
  | 5 | Data disclosure | Know what leaves the machine (LLM, Firecrawl, GitHub, chain RPCs) |
171
171
  | 6 | Server monitors | Scheduled jobs need explicit confirmation |
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ // Shared substring/keyword scoring for the local-file backends
3
+ // (local-memory-file.ts, local-vault.ts) - no embeddings, no server, zero
4
+ // dependencies. Word-boundary aware, not raw substring counting - a short
5
+ // common term like "is"/"a" used to score a "match" purely by being a
6
+ // substring of an unrelated word ("is" inside "distances", "a" inside
7
+ // "banana"). Found via real testing of memory_add's new conflict-hint
8
+ // feature: a query for "the user's favorite pizza topping is pepperoni"
9
+ // registered as "related" to a completely unrelated stored memory about
10
+ // metric vs imperial units, purely because both contained "the" and "is".
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.meaningfulTerms = meaningfulTerms;
13
+ exports.wordOccurrences = wordOccurrences;
14
+ const STOP_WORDS = new Set([
15
+ "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
16
+ "to", "of", "in", "on", "at", "for", "with", "by", "from", "as",
17
+ "and", "or", "but", "if", "so", "this", "that", "it", "its", "i",
18
+ "you", "your", "they", "them", "their", "he", "she", "his", "her",
19
+ "not", "no", "do", "does", "did", "has", "have", "had", "will", "would",
20
+ ]);
21
+ /** Query terms worth scoring against - lowercased, stop-words and
22
+ * single-character noise dropped. An empty result means the query was
23
+ * entirely stop words/punctuation - callers should treat that as "no
24
+ * meaningful query" (return no matches) rather than matching everything. */
25
+ function meaningfulTerms(query) {
26
+ return query
27
+ .toLowerCase()
28
+ .split(/[^a-z0-9]+/)
29
+ .filter((t) => t.length > 1 && !STOP_WORDS.has(t));
30
+ }
31
+ function escapeRegex(s) {
32
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+ /** Word-boundary occurrence count of `term` inside `haystack` - not a raw
35
+ * substring count, so "is" doesn't match inside "distances" or "this". */
36
+ function wordOccurrences(haystack, term) {
37
+ const re = new RegExp(`\\b${escapeRegex(term)}\\b`, "g");
38
+ return (haystack.match(re) || []).length;
39
+ }
@@ -44,6 +44,7 @@ const fs = __importStar(require("fs"));
44
44
  const os = __importStar(require("os"));
45
45
  const path = __importStar(require("path"));
46
46
  const crypto = __importStar(require("crypto"));
47
+ const _text_search_js_1 = require("./_text-search.js");
47
48
  function getLocalMemoryFileConfig() {
48
49
  return { dir: path.join(os.homedir(), ".finch", "memory") };
49
50
  }
@@ -107,7 +108,9 @@ function fileMemoryDeleteByVaultKey(cfg, vaultKey) {
107
108
  }
108
109
  function fileMemorySearch(cfg, query, limit) {
109
110
  const idx = readIndex(cfg);
110
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
111
+ const terms = (0, _text_search_js_1.meaningfulTerms)(query);
112
+ if (terms.length === 0)
113
+ return [];
111
114
  const scored = [];
112
115
  for (const m of idx.memories) {
113
116
  const hay = `${m.title ?? ""}\n${(m.tags ?? []).join(" ")}\n${m.content}`.toLowerCase();
@@ -117,7 +120,7 @@ function fileMemorySearch(cfg, query, limit) {
117
120
  score += 3;
118
121
  if ((m.tags ?? []).some((tag) => tag.toLowerCase().includes(t)))
119
122
  score += 2;
120
- score += Math.min(hay.split(t).length - 1, 5);
123
+ score += Math.min((0, _text_search_js_1.wordOccurrences)(hay, t), 5);
121
124
  }
122
125
  if (score > 0)
123
126
  scored.push({ m, score });
@@ -53,6 +53,7 @@ const os = __importStar(require("os"));
53
53
  const path = __importStar(require("path"));
54
54
  const crypto = __importStar(require("crypto"));
55
55
  const config_js_1 = require("./config.js");
56
+ const _text_search_js_1 = require("./_text-search.js");
56
57
  // Fully-local, user-owned Noel-Vault backend. Mirrors the two-tier pattern of
57
58
  // local-memory.ts: when the user opts in (`vaultBackend: "local"`), the vault
58
59
  // tools store versioned artifacts on the user's own disk under
@@ -232,8 +233,10 @@ function localVaultList(cfg, opts) {
232
233
  // ── search (full-text, local) ─────────────────────────────────────────────────
233
234
  function localVaultSearch(cfg, query, opts) {
234
235
  const idx = readIndex(cfg);
235
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
236
+ const terms = (0, _text_search_js_1.meaningfulTerms)(query);
236
237
  const scored = [];
238
+ if (terms.length === 0)
239
+ return { results: [] };
237
240
  for (const e of Object.values(idx.entries)) {
238
241
  if (e.type === "credential")
239
242
  continue;
@@ -247,8 +250,7 @@ function localVaultSearch(cfg, query, opts) {
247
250
  score += 3;
248
251
  if (e.tags.some((tag) => tag.toLowerCase().includes(t)))
249
252
  score += 2;
250
- const occurrences = hay.split(t).length - 1;
251
- score += Math.min(occurrences, 5);
253
+ score += Math.min((0, _text_search_js_1.wordOccurrences)(hay, t), 5);
252
254
  }
253
255
  if (score > 0) {
254
256
  const firstHit = terms.map((t) => content.toLowerCase().indexOf(t)).filter((i) => i >= 0).sort((a, b) => a - b)[0] ?? 0;
@@ -49,6 +49,7 @@ const convex_js_1 = require("../convex.js");
49
49
  const public_url_js_1 = require("../public-url.js");
50
50
  const local_vault_js_1 = require("../local-vault.js");
51
51
  const local_memory_js_1 = require("../local-memory.js");
52
+ const _text_search_js_1 = require("../_text-search.js");
52
53
  // memory_extract and memory_consolidate used to run their own LLM calls here
53
54
  // (and a matching pair of Convex routes did the same server-side). Both are now
54
55
  // two-pass: the tool fetches and stores, the caller decides what the facts are
@@ -304,7 +305,11 @@ exports.MEMORY_TOOLS = [
304
305
  "searchable in ~30s. Retrieval is full-text (keyword) search, not embeddings - " +
305
306
  "'what did I say about ETH yield?' finds notes containing those words or close variants, " +
306
307
  "not unrelated phrasing with the same meaning. " +
307
- "Auto-deduplicates: identical content in your recent 50 memories is skipped (override with force:true).",
308
+ "Auto-deduplicates: identical content in your recent 50 memories is skipped (override with force:true). " +
309
+ "Also surfaces up to 3 existing memories that share real keyword overlap with what you just saved (a " +
310
+ "possible-conflict HINT, not a verdict - Finch doesn't call an LLM to judge this). When that shows up, " +
311
+ "read them and decide yourself whether the new one supersedes an old preference/fact; if so, say so in a " +
312
+ "follow-up memory_add, or fold both into one with memory_consolidate.",
308
313
  inputSchema: {
309
314
  type: "object",
310
315
  properties: {
@@ -563,6 +568,46 @@ async function handleMemoryTool(name, args) {
563
568
  // dedupes even before supermemory has indexed the first one.
564
569
  if (!sourceUrl)
565
570
  rememberRecentHash(hash, data?.id ?? "saved", title);
571
+ // ─── Conflict hint (not a proof) ──────────────────────────────────
572
+ // Finch never runs its own LLM call to judge this - "two-pass, no API
573
+ // key needed" is deliberate (see this file's header comment), and an
574
+ // autonomous contradiction call would break that. Instead this just
575
+ // resurfaces whatever memory_search's own retrieval already ranks as
576
+ // related to the content just saved, so the CALLING model - already
577
+ // reasoning about this exact save, in the same turn, no extra API
578
+ // cost - can judge for itself whether the two actually conflict and
579
+ // decide what to do (mark the old one superseded, fold both into
580
+ // memory_consolidate, or just note the new one is the current one).
581
+ // Best-effort: a search hiccup here must never fail or block the save
582
+ // that already succeeded above.
583
+ let conflictNote = "";
584
+ try {
585
+ const related = await hybridMemorySearch(content, 8);
586
+ // hybridMemorySearch's score is rank-based and normalized per-call -
587
+ // a single weak match (one common word) can still come back as
588
+ // "top result, 100%" purely for lack of competition. Found live:
589
+ // "the user's favorite pizza topping" registered as "related" to a
590
+ // completely unrelated units-preference memory, because both
591
+ // happened to say "user". Counting actual shared meaningful terms
592
+ // (>=2 distinct, not stop words) is a real, absolute bar instead of
593
+ // trusting a relative score - "user" alone no longer qualifies,
594
+ // "user"+"prefers"+"units"+"metric"/"imperial" does.
595
+ const newTerms = new Set((0, _text_search_js_1.meaningfulTerms)(content));
596
+ const candidates = related
597
+ .filter((r) => r.id !== data?.id && contentHash(r.content) !== hash)
598
+ .map((r) => ({ r, overlap: new Set((0, _text_search_js_1.meaningfulTerms)(r.content).filter((t) => newTerms.has(t))).size }))
599
+ .filter((x) => x.overlap >= 2)
600
+ .sort((a, b) => b.overlap - a.overlap)
601
+ .slice(0, 3)
602
+ .map((x) => x.r);
603
+ if (candidates.length) {
604
+ conflictNote = "\n\n⚠️ **Possibly related existing memories** - skim these for a conflict with what you just saved (e.g. an old preference this replaces):\n" +
605
+ candidates.map((r) => `- \`${r.id}\`: ${r.content.slice(0, 100)}${r.content.length > 100 ? "…" : ""}`).join("\n");
606
+ }
607
+ }
608
+ catch {
609
+ // best-effort only
610
+ }
566
611
  return {
567
612
  content: [{
568
613
  type: "text",
@@ -573,7 +618,7 @@ async function handleMemoryTool(name, args) {
573
618
  tags?.length ? `Tags: ${tags.join(", ")}` : "",
574
619
  ``,
575
620
  `Find it with: \`memory_search query: "${(title ?? content).slice(0, 40)}"\``,
576
- ].filter(Boolean).join("\n"),
621
+ ].filter(Boolean).join("\n") + conflictNote,
577
622
  }],
578
623
  };
579
624
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finchagentic/mcp",
3
- "version": "4.5.0",
3
+ "version": "4.6.1",
4
4
  "description": "The runtime layer for Agentic AI. Persistent memory, autonomous agents, and workflows that survive every session.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {