@finchagentic/mcp 4.6.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 +7 -7
- package/dist/_text-search.js +39 -0
- package/dist/local-memory-file.js +5 -2
- package/dist/local-vault.js +5 -3
- package/dist/tools/memory.js +17 -2
- package/package.json +1 -1
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.6.
|
|
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.6.
|
|
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.6.0 finch-mcp
|
|
|
45
45
|
"mcpServers": {
|
|
46
46
|
"finch": {
|
|
47
47
|
"command": "npx",
|
|
48
|
-
"args": ["-y", "-p", "@finchagentic/mcp@4.6.
|
|
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.6.0 finch-mcp
|
|
|
59
59
|
"finch": {
|
|
60
60
|
"type": "stdio",
|
|
61
61
|
"command": "npx",
|
|
62
|
-
"args": ["-y", "-p", "@finchagentic/mcp@4.6.
|
|
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.6.
|
|
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.6.
|
|
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.6.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.6.
|
|
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 =
|
|
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(
|
|
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 });
|
package/dist/local-vault.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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;
|
package/dist/tools/memory.js
CHANGED
|
@@ -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
|
|
@@ -581,10 +582,24 @@ async function handleMemoryTool(name, args) {
|
|
|
581
582
|
// that already succeeded above.
|
|
582
583
|
let conflictNote = "";
|
|
583
584
|
try {
|
|
584
|
-
const related = await hybridMemorySearch(content,
|
|
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));
|
|
585
596
|
const candidates = related
|
|
586
597
|
.filter((r) => r.id !== data?.id && contentHash(r.content) !== hash)
|
|
587
|
-
.
|
|
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);
|
|
588
603
|
if (candidates.length) {
|
|
589
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" +
|
|
590
605
|
candidates.map((r) => `- \`${r.id}\`: ${r.content.slice(0, 100)}${r.content.length > 100 ? "…" : ""}`).join("\n");
|
package/package.json
CHANGED