@appsoftwareltd/etherpk-mcp 0.4.0 → 0.4.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 +4 -0
- package/dist/main.js +88 -7
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,10 @@ model files into `~/.cache/etherpk/mcp/models/all-MiniLM-L6-v2-int8/`.
|
|
|
97
97
|
after that only edits are processed. `semantic status` lists each cached graph with how many
|
|
98
98
|
passages are done; every semantic result says `embedded`/`total` and `complete`; run by
|
|
99
99
|
hand, `serve` prints a line every 30 seconds. `semantic remove` deletes runtime and model.
|
|
100
|
+
- **Load on the machine.** The first pass uses a quarter of the cores, at most four, and pauses
|
|
101
|
+
between model calls; `ETHERPK_MCP_SEMANTIC_THREADS=8` in the registration's `env` makes it
|
|
102
|
+
faster and hotter. `ETHERPK_MCP_DEBUG_MEMORY=1` logs the process's memory if you ever need
|
|
103
|
+
to see it.
|
|
100
104
|
- **One cache directory for both.** Setup and `serve` must see the same cache root - by default
|
|
101
105
|
`~/.cache/etherpk/mcp` (`C:\Users\<you>\.cache\etherpk\mcp` on Windows). If you set
|
|
102
106
|
`ETHERPK_MCP_CACHE_DIR` in your shell, put it in the agent registration's `env` too, since
|
package/dist/main.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parse, stringify } from "yaml";
|
|
|
25
25
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26
26
|
var package_default = {
|
|
27
27
|
name: "@appsoftwareltd/etherpk-mcp",
|
|
28
|
-
version: "0.4.
|
|
28
|
+
version: "0.4.1",
|
|
29
29
|
license: "Elastic-2.0",
|
|
30
30
|
description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
|
|
31
31
|
type: "module",
|
|
@@ -741,8 +741,11 @@ function normaliseSyncServer(value) {
|
|
|
741
741
|
}
|
|
742
742
|
/**
|
|
743
743
|
* Tolerant of a hand-edited file: keeps the entries it knows, refuses the rest. A file in the
|
|
744
|
-
* single-login shape written before 0.3.0
|
|
745
|
-
*
|
|
744
|
+
* single-login shape written before 0.3.0 - `{ syncServer, pat, vaultKey }` at the top level -
|
|
745
|
+
* is read as that one login under its server: an agent registered under 0.2.0 must keep
|
|
746
|
+
* working when npx pulls a newer version, and refusing the file showed the user only
|
|
747
|
+
* "Connection closed" in Claude Code (2026-09-17). The next `login` or `logout` rewrites it in
|
|
748
|
+
* the current shape.
|
|
746
749
|
*/
|
|
747
750
|
function parseConfig(raw) {
|
|
748
751
|
let parsed;
|
|
@@ -752,6 +755,14 @@ function parseConfig(raw) {
|
|
|
752
755
|
return null;
|
|
753
756
|
}
|
|
754
757
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
758
|
+
const legacy = parsed;
|
|
759
|
+
if (typeof legacy.syncServer === "string" && typeof legacy.pat === "string" && legacy.pat !== "" && !("servers" in legacy)) {
|
|
760
|
+
const syncServer = normaliseSyncServer(legacy.syncServer);
|
|
761
|
+
if (!/^https?:\/\//.test(syncServer)) return null;
|
|
762
|
+
const login = { pat: legacy.pat };
|
|
763
|
+
if (typeof legacy.vaultKey === "string" && legacy.vaultKey !== "") login.vaultKey = legacy.vaultKey;
|
|
764
|
+
return { servers: { [syncServer]: login } };
|
|
765
|
+
}
|
|
755
766
|
const servers = parsed.servers;
|
|
756
767
|
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return null;
|
|
757
768
|
const out = emptyConfig();
|
|
@@ -3338,13 +3349,38 @@ async function openCacheDb() {
|
|
|
3338
3349
|
}
|
|
3339
3350
|
return db;
|
|
3340
3351
|
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Give every typed array in a row its own exact-size buffer.
|
|
3354
|
+
*
|
|
3355
|
+
* Node's v8 deserialiser hands typed arrays back as zero-copy VIEWS on the buffer it was given
|
|
3356
|
+
* - here the whole snapshot file - and IndexedDB's structured clone copies a view's entire
|
|
3357
|
+
* underlying buffer, not the view. Restoring 2,474 rows that all pointed into one 11 MB file
|
|
3358
|
+
* therefore cloned 11 MB per row: 28 GB resident, the machine in swap, a thermal shutdown
|
|
3359
|
+
* (2026-09-17). Capture copies too, so a snapshot never carries more than the bytes it means
|
|
3360
|
+
* whatever the store handed back. Plain Uint8Arrays, which is what the engine reads.
|
|
3361
|
+
*/
|
|
3362
|
+
function withStandaloneBuffers(value) {
|
|
3363
|
+
if (ArrayBuffer.isView(value)) {
|
|
3364
|
+
const view = value;
|
|
3365
|
+
const copy = new Uint8Array(view.byteLength);
|
|
3366
|
+
copy.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
3367
|
+
return copy;
|
|
3368
|
+
}
|
|
3369
|
+
if (Array.isArray(value)) return value.map(withStandaloneBuffers);
|
|
3370
|
+
if (value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
3371
|
+
const out = {};
|
|
3372
|
+
for (const [key, entry] of Object.entries(value)) out[key] = withStandaloneBuffers(entry);
|
|
3373
|
+
return out;
|
|
3374
|
+
}
|
|
3375
|
+
return value;
|
|
3376
|
+
}
|
|
3341
3377
|
/** Every row of every cache store that belongs to `graphId`, as one serialisable snapshot. */
|
|
3342
3378
|
async function captureLocalCache(graphId) {
|
|
3343
3379
|
const db = await openCacheDb();
|
|
3344
3380
|
try {
|
|
3345
3381
|
const tx = db.transaction([...CACHE_STORES], "readonly");
|
|
3346
3382
|
const rows = {};
|
|
3347
|
-
for (const store of CACHE_STORES) rows[store] = (await request(tx.objectStore(store).getAll())).filter((row) => row.graphId === graphId);
|
|
3383
|
+
for (const store of CACHE_STORES) rows[store] = (await request(tx.objectStore(store).getAll())).filter((row) => row.graphId === graphId).map(withStandaloneBuffers);
|
|
3348
3384
|
await done(tx);
|
|
3349
3385
|
return {
|
|
3350
3386
|
version: CACHE_DB_VERSION,
|
|
@@ -3362,7 +3398,7 @@ async function restoreLocalCache(snapshot) {
|
|
|
3362
3398
|
const tx = db.transaction([...CACHE_STORES], "readwrite");
|
|
3363
3399
|
let count = 0;
|
|
3364
3400
|
for (const store of CACHE_STORES) for (const row of snapshot.rows[store]) {
|
|
3365
|
-
tx.objectStore(store).put(row);
|
|
3401
|
+
tx.objectStore(store).put(withStandaloneBuffers(row));
|
|
3366
3402
|
count++;
|
|
3367
3403
|
}
|
|
3368
3404
|
await done(tx);
|
|
@@ -3409,6 +3445,28 @@ async function loadLocalCache(dir, graphId) {
|
|
|
3409
3445
|
return 0;
|
|
3410
3446
|
}
|
|
3411
3447
|
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Remove what an older build or a killed write left in a graph's directory: index and vectors
|
|
3450
|
+
* files stamped with another version (a bump renames them, so they are never opened again and
|
|
3451
|
+
* were 52 MB of litter per graph), and `.tmp` files from an atomic write that never reached its
|
|
3452
|
+
* rename. The files this build reads are left alone.
|
|
3453
|
+
*/
|
|
3454
|
+
async function removeStaleFiles(dir) {
|
|
3455
|
+
const keep = new Set([
|
|
3456
|
+
cacheFile(dir),
|
|
3457
|
+
indexFile(dir),
|
|
3458
|
+
vectorsFile(dir)
|
|
3459
|
+
].map((f) => f.split("/").pop()));
|
|
3460
|
+
const removed = [];
|
|
3461
|
+
for (const name of await readdir(dir).catch(() => [])) {
|
|
3462
|
+
const stale = /^(index|vectors)\.v\d+\.sqlite$/.test(name) && !keep.has(name);
|
|
3463
|
+
const abandoned = /\.\d+\.tmp$/.test(name);
|
|
3464
|
+
if (!stale && !abandoned) continue;
|
|
3465
|
+
await rm(join(dir, name), { force: true });
|
|
3466
|
+
removed.push(name);
|
|
3467
|
+
}
|
|
3468
|
+
return removed;
|
|
3469
|
+
}
|
|
3412
3470
|
/** Hosts opened in this process, so each gets its own paths in sqlite-wasm's filesystem. */
|
|
3413
3471
|
var hostSerial = 0;
|
|
3414
3472
|
function nodeIndexHost(dir) {
|
|
@@ -3488,6 +3546,7 @@ function nodeIndexHost(dir) {
|
|
|
3488
3546
|
return {
|
|
3489
3547
|
async open() {
|
|
3490
3548
|
const s = await init();
|
|
3549
|
+
await removeStaleFiles(dir);
|
|
3491
3550
|
await importVectors(s);
|
|
3492
3551
|
live = await openSaved(s) ?? openFresh(s);
|
|
3493
3552
|
return {
|
|
@@ -3785,6 +3844,13 @@ var SemanticUnavailable = class extends Error {
|
|
|
3785
3844
|
}
|
|
3786
3845
|
};
|
|
3787
3846
|
var SETUP_HINT = "run `npx @appsoftwareltd/etherpk-mcp semantic setup` on this computer once (it installs a ~300 MB runtime and a 23 MB model into the cache directory); the next semantic search will use it, no restart needed.";
|
|
3847
|
+
/** The thread count `serve` uses: the option, else the environment, else the conservative default. */
|
|
3848
|
+
function semanticThreads(env, requested) {
|
|
3849
|
+
if (requested !== void 0) return Math.max(1, Math.floor(requested));
|
|
3850
|
+
const fromEnv = Number(env.ETHERPK_MCP_SEMANTIC_THREADS);
|
|
3851
|
+
if (Number.isInteger(fromEnv) && fromEnv >= 1) return fromEnv;
|
|
3852
|
+
return Math.max(1, Math.min(4, Math.floor(availableParallelism() / 4)));
|
|
3853
|
+
}
|
|
3788
3854
|
/**
|
|
3789
3855
|
* Load the runtime from the cache directory and open the model. Resident memory is a few
|
|
3790
3856
|
* hundred megabytes, so `serve` calls this only when semantic mode is set up on the machine.
|
|
@@ -3801,7 +3867,7 @@ async function loadEmbeddingModel(env, options = {}) {
|
|
|
3801
3867
|
throw new SemanticUnavailable(`The embedding runtime in ${status.runtimeDir} failed to load (${error instanceof Error ? error.message : String(error)}); ${SETUP_HINT}`);
|
|
3802
3868
|
}
|
|
3803
3869
|
const tokenizer = new Tokenizer(JSON.parse(await readFile(join(status.modelDir, "tokenizer.json"), "utf8")), JSON.parse(await readFile(join(status.modelDir, "tokenizer_config.json"), "utf8")));
|
|
3804
|
-
const threads =
|
|
3870
|
+
const threads = semanticThreads(env, options.threads);
|
|
3805
3871
|
const session = await ort.InferenceSession.create(join(status.modelDir, "model.onnx"), {
|
|
3806
3872
|
intraOpNumThreads: threads,
|
|
3807
3873
|
interOpNumThreads: 1,
|
|
@@ -7492,6 +7558,7 @@ function createSemanticIndex(options) {
|
|
|
7492
7558
|
const { index, model } = options;
|
|
7493
7559
|
const floor = options.floor ?? .25;
|
|
7494
7560
|
const settleMs = options.settleMs ?? 1500;
|
|
7561
|
+
const pauseMs = options.pauseMs ?? 100;
|
|
7495
7562
|
let disposed = false;
|
|
7496
7563
|
let running;
|
|
7497
7564
|
let again = false;
|
|
@@ -7512,6 +7579,7 @@ function createSemanticIndex(options) {
|
|
|
7512
7579
|
hash: passage.hash,
|
|
7513
7580
|
...quantise(vectors[n])
|
|
7514
7581
|
}));
|
|
7582
|
+
if (pauseMs > 0) await new Promise((resolve) => setTimeout(resolve, pauseMs));
|
|
7515
7583
|
}
|
|
7516
7584
|
await index.semantic.put(model.id, model.dims, rows);
|
|
7517
7585
|
if (options.onProgress) options.onProgress(await index.semantic.status(model.id));
|
|
@@ -9796,8 +9864,21 @@ function reportSemanticProgress(status) {
|
|
|
9796
9864
|
if (!upToDate && Date.now() - lastProgressLog < 3e4) return;
|
|
9797
9865
|
lastProgressLog = Date.now();
|
|
9798
9866
|
announcedUpToDate = upToDate;
|
|
9799
|
-
console.error(`etherpk-mcp: semantic: ${status.embedded} of ${status.total} passages embedded${upToDate ? " - up to date" : ""}
|
|
9867
|
+
console.error(`etherpk-mcp: semantic: ${status.embedded} of ${status.total} passages embedded${upToDate ? " - up to date" : ""}.${memoryNote()}`);
|
|
9868
|
+
}
|
|
9869
|
+
/**
|
|
9870
|
+
* `ETHERPK_MCP_DEBUG_MEMORY=1` adds the process's memory to every progress line and logs it every
|
|
9871
|
+
* ten seconds: the numbers that separate the JavaScript heap, the buffers outside it and the
|
|
9872
|
+
* native runtime when a serve grows without reason (2026-09-17).
|
|
9873
|
+
*/
|
|
9874
|
+
var debugMemory = process.env.ETHERPK_MCP_DEBUG_MEMORY === "1";
|
|
9875
|
+
function memoryNote() {
|
|
9876
|
+
if (!debugMemory) return "";
|
|
9877
|
+
const m = process.memoryUsage();
|
|
9878
|
+
const mb = (n) => Math.round(n / 1e6);
|
|
9879
|
+
return ` [rss ${mb(m.rss)} MB, heap ${mb(m.heapUsed)} MB, external ${mb(m.external)} MB, arrayBuffers ${mb(m.arrayBuffers)} MB]`;
|
|
9800
9880
|
}
|
|
9881
|
+
if (debugMemory) setInterval(() => console.error(`etherpk-mcp: memory${memoryNote()}`), 1e4).unref();
|
|
9801
9882
|
async function serve(args) {
|
|
9802
9883
|
const wanted = args.graph?.trim();
|
|
9803
9884
|
if (!wanted) fail("serve needs --graph <id or name>.");
|