memtrails 0.4.3 → 0.5.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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ versions follow [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.5.1] — 2026-11-05
9
+
10
+ - Rust: the MCP server and the command line, as in TypeScript and Python — `cargo install memtrails`, then
11
+ `memtrails mcp`, `add`, `search`; `memtrails::mcp::serve_mcp` from code, held to the same answers
12
+ (`conformance/fixtures/mcp.json`). The TypeScript and Python versions are released at the same version, unchanged.
13
+
14
+ ## [0.5.0] — 2026-11-05
15
+
16
+ - An MCP server and a command line: `npx memtrails mcp` serves a memory to Claude Desktop, Claude Code, Cursor and any
17
+ MCP client (`memory_search`, `memory_open`, `memory_explore`, and `memory_save` unless `--read-only`), opening it at
18
+ the first tool call and closing it when idle; `memtrails add` fills it from folders, mail, JSONL, a Slack export or
19
+ git; `memtrails search` asks it. `serveMcp` serves one from code. The same in Python (`uvx memtrails mcp`,
20
+ `serve_mcp`), held to the same answers (`conformance/fixtures/mcp.json`). The Rust version has the same library at
21
+ this version, without the server and command line for now: its `Memory::tools()` serve an MCP server of your own.
22
+
8
23
  ## [0.4.3] — 2026-11-05
9
24
 
10
25
  - Fixes: the first start no longer fails with "Qdrant exited (101) while starting" where the system has no CA
package/README.md CHANGED
@@ -45,8 +45,8 @@ await memory.close();
45
45
  answer an independent audit found wrong. [Benchmarks](docs/BENCHMARKS.md).
46
46
  - **Explorable.** A concept graph links what records share — people, places, systems, names — so an agent can walk
47
47
  from what it found to what a search for the words does not find.
48
- - **Built for agents.** `memory.tools()` gives `memory_search`, `memory_open` and `memory_explore` with zod and JSON
49
- schemas, ready for an MCP server or any function-calling API.
48
+ - **Built for agents.** `npx memtrails mcp` gives Claude, Cursor or any MCP app a memory it can search and save to;
49
+ `memory.tools()` gives the same tools with zod and JSON schemas, for your own server or any function-calling API.
50
50
 
51
51
  ## Install
52
52
 
@@ -69,6 +69,33 @@ pip install memtrails # Python 3.10–3.13, asyncio — see python/READ
69
69
  cargo add memtrails # Rust 1.88+, tokio — see rust/README.md
70
70
  ```
71
71
 
72
+ ## Memory for Claude, Cursor and other MCP apps
73
+
74
+ One line gives an MCP client a long-term memory on your machine: `memory_search`, `memory_open`, `memory_explore`,
75
+ and `memory_save` for what the agent should remember.
76
+
77
+ ```bash
78
+ claude mcp add memory -- npx -y memtrails mcp # Claude Code
79
+ ```
80
+
81
+ ```json
82
+ { "mcpServers": { "memory": { "command": "npx", "args": ["-y", "memtrails", "mcp"] } } }
83
+ ```
84
+
85
+ The second is for Claude Desktop (`claude_desktop_config.json`), Cursor (`.cursor/mcp.json`) and most other clients.
86
+ The same server comes with the Python and Rust versions: `uvx memtrails mcp`, or `cargo install memtrails` and then
87
+ `memtrails mcp`. Fill the memory from the command line, and try it:
88
+
89
+ ```bash
90
+ npx memtrails add ~/notes ~/mail/archive.mbox ./slack-export # notes and documents, mail, a Slack export
91
+ npx memtrails add --git ~/code/my-repo # commit messages and changed files
92
+ npx memtrails search "why did the certificate stop renewing?"
93
+ ```
94
+
95
+ The memory lives in `~/.memtrails` (`--home` or `MEMTRAILS_HOME` for another). The server opens it at the first tool
96
+ call and closes it after five minutes without one (`--idle`), so `add` can run in between; `--read-only` leaves out
97
+ `memory_save`. From code: `serveMcp({ open: () => openMemory({ home }), version })`.
98
+
72
99
  ## Adding data
73
100
 
74
101
  A **record** is anything with text. Only `id` and `text` are required.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * memtrails on the command line: `mcp` serves a memory to an MCP client, `add` fills it from files, mail, Slack or
4
+ * git, `search` asks it. The memory is MEMTRAILS_HOME, or ~/.memtrails, unless --home names another.
5
+ */
6
+ import { existsSync, statSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import { homedir } from 'node:os';
9
+ import { extname, join, resolve } from 'node:path';
10
+ import { fromEml, fromFiles, fromGit, fromJsonl, fromMbox, fromSlackExport } from './importers/index.js';
11
+ import { serveMcp } from './mcp.js';
12
+ import { openMemory } from './memory.js';
13
+ const VERSION = createRequire(import.meta.url)('../package.json').version;
14
+ const USAGE = `memtrails ${VERSION} — local memory for agents
15
+
16
+ memtrails mcp [--read-only] [--idle MINUTES] serve the memory to an MCP client over stdio
17
+ memtrails add PATH... [--git REPO] [--collection NAME]
18
+ add folders of notes and documents, .mbox/.eml mail,
19
+ .jsonl records, a Slack export folder, a git history
20
+ memtrails search QUERY... [--limit N] search, best first
21
+
22
+ --home DIR the memory (default: MEMTRAILS_HOME, or ~/.memtrails)
23
+
24
+ MCP client configuration (Claude Desktop, Cursor, …):
25
+ { "mcpServers": { "memory": { "command": "npx", "args": ["-y", "memtrails", "mcp"] } } }
26
+ Claude Code:
27
+ claude mcp add memory -- npx -y memtrails mcp
28
+ `;
29
+ const VALUED = new Set(['--home', '--git', '--collection', '--limit', '--idle']);
30
+ const SWITCHES = new Set(['--read-only', '--help', '-h', '--version', '-v']);
31
+ function parse(argv) {
32
+ const flags = new Map();
33
+ const switches = new Set();
34
+ const rest = [];
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ const eq = a.indexOf('=');
38
+ const name = a.startsWith('--') && eq > 0 ? a.slice(0, eq) : a;
39
+ if (VALUED.has(name)) {
40
+ const value = eq > 0 && a.startsWith('--') ? a.slice(eq + 1) : argv[++i];
41
+ if (value === undefined)
42
+ throw new Error(`${name} needs a value`);
43
+ flags.set(name, [...(flags.get(name) ?? []), value]);
44
+ }
45
+ else if (SWITCHES.has(a)) {
46
+ switches.add(a);
47
+ }
48
+ else if (a.startsWith('-') && a !== '-') {
49
+ throw new Error(`unknown option ${a}`);
50
+ }
51
+ else {
52
+ rest.push(a);
53
+ }
54
+ }
55
+ return { command: rest.shift() ?? '', rest, flags, switches };
56
+ }
57
+ const one = (a, name) => a.flags.get(name)?.at(-1);
58
+ const homeOf = (a) => resolve(one(a, '--home') ?? process.env.MEMTRAILS_HOME ?? join(homedir(), '.memtrails'));
59
+ const say = (line) => { process.stderr.write(`${line}\n`); };
60
+ function number(a, name, fallback, min) {
61
+ const raw = one(a, name);
62
+ if (raw === undefined)
63
+ return fallback;
64
+ const n = Number(raw);
65
+ if (!Number.isFinite(n) || n < min)
66
+ throw new Error(`${name} must be a number of at least ${min}`);
67
+ return n;
68
+ }
69
+ /** What a path holds, as records: the importer its kind calls for. */
70
+ function importerFor(path, collection) {
71
+ if (!existsSync(path))
72
+ throw new Error(`${path}: not found`);
73
+ if (statSync(path).isDirectory()) {
74
+ if (existsSync(join(path, 'channels.json')) && existsSync(join(path, 'users.json')))
75
+ return fromSlackExport(path, { collection });
76
+ return fromFiles(path, { collection, onSkip: (p, why) => say(` skipped ${p}: ${why}`) });
77
+ }
78
+ switch (extname(path).toLowerCase()) {
79
+ case '.mbox': return fromMbox(path, { collection });
80
+ case '.eml': return fromEml(path, { collection });
81
+ case '.jsonl': return fromJsonl(path, { collection, onSkip: (_line, why, n) => say(` skipped line ${n}: ${why}`) });
82
+ default: throw new Error(`${path}: give a folder of notes or documents, an .mbox or .eml file, a .jsonl file, or a Slack export folder`);
83
+ }
84
+ }
85
+ async function withMemory(a, f) {
86
+ const memory = await openMemory({ home: homeOf(a), log: say });
87
+ try {
88
+ return await f(memory);
89
+ }
90
+ finally {
91
+ await memory.close();
92
+ }
93
+ }
94
+ async function add(a) {
95
+ const collection = one(a, '--collection');
96
+ const sources = [
97
+ ...a.rest.map((p) => [p, importerFor(resolve(p), collection)]),
98
+ ...(a.flags.get('--git') ?? []).map((p) => [`${p} (git)`, fromGit(resolve(p), { collection })]),
99
+ ];
100
+ if (!sources.length)
101
+ throw new Error('add what? give one or more paths, or --git REPO');
102
+ await withMemory(a, async (m) => {
103
+ for (const [name, records] of sources) {
104
+ say(`adding ${name}`);
105
+ const r = await m.import(records, { onProgress: (n) => say(` ${n} records`) });
106
+ say(` ${r.added} added, ${r.unchanged} unchanged`);
107
+ }
108
+ const s = m.stats();
109
+ say(`${homeOf(a)}: ${s.records} records`);
110
+ });
111
+ }
112
+ async function search(a) {
113
+ const query = a.rest.join(' ').trim();
114
+ if (!query)
115
+ throw new Error('search for what? give the question as words');
116
+ const limit = number(a, '--limit', 10, 1);
117
+ await withMemory(a, async (m) => { process.stdout.write(`${await m.searchText({ query, limit })}\n`); });
118
+ }
119
+ async function mcp(a) {
120
+ const home = homeOf(a);
121
+ await serveMcp({
122
+ version: VERSION,
123
+ readOnly: a.switches.has('--read-only'),
124
+ idleMs: number(a, '--idle', 5, 0) * 60_000,
125
+ open: () => openMemory({ home, log: say }),
126
+ });
127
+ }
128
+ async function main() {
129
+ const a = parse(process.argv.slice(2));
130
+ if (a.switches.has('--version') || a.switches.has('-v'))
131
+ return void process.stdout.write(`${VERSION}\n`);
132
+ if (!a.command || a.command === 'help' || a.switches.has('--help') || a.switches.has('-h'))
133
+ return void process.stdout.write(USAGE);
134
+ switch (a.command) {
135
+ case 'mcp': return mcp(a);
136
+ case 'add': return add(a);
137
+ case 'search': return search(a);
138
+ default: throw new Error(`unknown command ${a.command}; try memtrails --help`);
139
+ }
140
+ }
141
+ // Ends on its own, never by process.exit: exiting while ONNX Runtime's threads are still there aborts the process.
142
+ main().catch((e) => {
143
+ say(`memtrails: ${e instanceof Error ? e.message : String(e)}`);
144
+ process.exitCode = 1;
145
+ });
package/dist/index.d.ts CHANGED
@@ -26,3 +26,4 @@ export type { RerankRoute } from './core/reranker.js';
26
26
  export { sentenceRoom, sentenceSpans } from './core/room.js';
27
27
  export { maxSim, packWords, WordStore, WORD_LIMIT, type WordEncoder, type WordVectors, type PackedWords } from './core/words.js';
28
28
  export { cacheDir } from './core/paths.js';
29
+ export { serveMcp, MCP_PROTOCOL_VERSIONS, type McpServerOptions } from './mcp.js';
package/dist/index.js CHANGED
@@ -20,3 +20,4 @@ export { ensureQdrant, QDRANT_VERSION } from './core/qdrant-binary.js';
20
20
  export { sentenceRoom, sentenceSpans } from './core/room.js';
21
21
  export { maxSim, packWords, WordStore, WORD_LIMIT } from './core/words.js';
22
22
  export { cacheDir } from './core/paths.js';
23
+ export { serveMcp, MCP_PROTOCOL_VERSIONS } from './mcp.js';
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ import type { Memory } from './memory.js';
3
+ /** The protocol versions this server speaks, newest first; a client asking for another gets the newest. */
4
+ export declare const MCP_PROTOCOL_VERSIONS: string[];
5
+ export interface McpServerOptions {
6
+ /** Opens the memory; called at the first tool call, and again after an idle close. */
7
+ open: () => Promise<Memory>;
8
+ /** Without memory_save. */
9
+ readOnly?: boolean;
10
+ /** Close the memory after this long without a tool call (default 5 minutes; 0 keeps it open). */
11
+ idleMs?: number;
12
+ /** The server's version, as clients show it. */
13
+ version: string;
14
+ input?: Readable;
15
+ output?: Writable;
16
+ log?: (line: string) => void;
17
+ }
18
+ /** Serves until the input ends, then closes the memory. Resolves when it is closed. */
19
+ export declare function serveMcp(o: McpServerOptions): Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * A memory as an MCP server over stdio (newline-delimited JSON-RPC), for Claude Desktop, Claude Code, Cursor and any
3
+ * other MCP client: the agent tools (memory_search, memory_open, memory_explore) and, unless read-only, memory_save.
4
+ *
5
+ * The memory is opened at the first tool call and closed again after `idleMs` without one, so the home is free in
6
+ * between: for `memtrails add`, or another client on the same memory. A memory another process holds is a tool error
7
+ * that says which process, not a server that will not start.
8
+ */
9
+ import { randomBytes } from 'node:crypto';
10
+ import { createInterface } from 'node:readline';
11
+ import { z } from 'zod';
12
+ import { buildAgentTools } from './tools/agent-tools.js';
13
+ /** The protocol versions this server speaks, newest first; a client asking for another gets the newest. */
14
+ export const MCP_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
15
+ const INSTRUCTIONS = 'Long-term memory: what this person or team worked on, wrote, decided and said, and what was learned '
16
+ + 'from it. Search it before answering about past work, people, decisions or dates, and open a result to read it whole. '
17
+ + 'Save what is worth remembering later: a decision and why, a preference, a fact about a person or a system.';
18
+ const SAVE_DESCRIPTION = 'Save something to memory, to be found later by memory_search: a decision and why, a preference, '
19
+ + 'a fact about a person, a project or a system, what was learned. Write it to be understood on its own, later, by '
20
+ + 'someone who was not here: name who and what, and the date when it matters.';
21
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
22
+ /** Serves until the input ends, then closes the memory. Resolves when it is closed. */
23
+ export async function serveMcp(o) {
24
+ const input = o.input ?? process.stdin;
25
+ const output = o.output ?? process.stdout;
26
+ const log = o.log ?? ((line) => process.stderr.write(`${line}\n`));
27
+ const idleMs = o.idleMs ?? 5 * 60_000;
28
+ let memory = null;
29
+ let busy = 0;
30
+ let idle = null;
31
+ const send = (message) => { output.write(`${JSON.stringify(message)}\n`); };
32
+ const closeMemory = async () => {
33
+ const m = memory;
34
+ memory = null;
35
+ if (m)
36
+ await m.then((x) => x.close(), () => undefined);
37
+ };
38
+ const touch = () => {
39
+ if (idle)
40
+ clearTimeout(idle);
41
+ idle = null;
42
+ if (idleMs > 0 && busy === 0 && memory) {
43
+ idle = setTimeout(() => { if (busy === 0)
44
+ void closeMemory().catch((e) => log(`[memtrails] close: ${String(e)}`)); }, idleMs);
45
+ idle.unref();
46
+ }
47
+ };
48
+ const withMemory = async (f) => {
49
+ busy++;
50
+ if (idle)
51
+ clearTimeout(idle);
52
+ try {
53
+ if (!memory) {
54
+ memory = o.open();
55
+ memory.catch(() => { memory = null; }); // the next call tries again
56
+ }
57
+ return await f(await memory);
58
+ }
59
+ finally {
60
+ busy--;
61
+ touch();
62
+ }
63
+ };
64
+ // The tools' definitions do not need an open memory: they are listed before any call.
65
+ const definitions = toolDefinitions(o.readOnly ?? false);
66
+ const call = async (name, args) => {
67
+ const d = definitions.find((t) => t.name === name);
68
+ if (!d)
69
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
70
+ try {
71
+ return await withMemory((m) => d.run(m, args));
72
+ }
73
+ catch (e) {
74
+ return { content: [{ type: 'text', text: e instanceof Error ? e.message : String(e) }], isError: true };
75
+ }
76
+ };
77
+ const handle = async (r) => {
78
+ const isCall = r.id !== undefined;
79
+ if (r.jsonrpc !== '2.0' || typeof r.method !== 'string') {
80
+ return isCall ? { jsonrpc: '2.0', id: r.id ?? null, error: { code: -32600, message: 'Invalid request' } } : undefined;
81
+ }
82
+ const ok = (result) => ({ jsonrpc: '2.0', id: r.id, result });
83
+ switch (r.method) {
84
+ case 'initialize': {
85
+ const asked = r.params?.protocolVersion;
86
+ const protocolVersion = typeof asked === 'string' && MCP_PROTOCOL_VERSIONS.includes(asked) ? asked : MCP_PROTOCOL_VERSIONS[0];
87
+ return ok({ protocolVersion, capabilities: { tools: {} }, serverInfo: { name: 'memtrails', version: o.version }, instructions: INSTRUCTIONS });
88
+ }
89
+ case 'ping':
90
+ return isCall ? ok({}) : undefined;
91
+ case 'tools/list':
92
+ return ok({ tools: definitions.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, annotations })) });
93
+ case 'tools/call': {
94
+ const name = r.params?.name;
95
+ if (typeof name !== 'string')
96
+ return { jsonrpc: '2.0', id: r.id, error: { code: -32602, message: 'tools/call needs a tool name' } };
97
+ return ok(await call(name, r.params?.arguments ?? {}));
98
+ }
99
+ default:
100
+ // Notifications (initialized, cancelled, …) need no answer; an unknown request does.
101
+ return isCall ? { jsonrpc: '2.0', id: r.id, error: { code: -32601, message: `Method not found: ${r.method}` } } : undefined;
102
+ }
103
+ };
104
+ const pending = new Set();
105
+ const lines = createInterface({ input, crlfDelay: Infinity });
106
+ for await (const line of lines) {
107
+ if (!line.trim())
108
+ continue;
109
+ let message;
110
+ try {
111
+ message = JSON.parse(line);
112
+ }
113
+ catch {
114
+ send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
115
+ continue;
116
+ }
117
+ // Answered as each finishes: a slow search does not hold up a ping.
118
+ const p = (async () => {
119
+ if (Array.isArray(message)) {
120
+ const answers = (await Promise.all(message.map((m) => handle(asRequest(m))))).filter((a) => a !== undefined);
121
+ if (answers.length)
122
+ send(answers);
123
+ }
124
+ else {
125
+ const answer = await handle(asRequest(message));
126
+ if (answer !== undefined)
127
+ send(answer);
128
+ }
129
+ })().catch((e) => log(`[memtrails] mcp: ${String(e)}`));
130
+ pending.add(p);
131
+ void p.finally(() => pending.delete(p));
132
+ }
133
+ await Promise.all(pending);
134
+ if (idle)
135
+ clearTimeout(idle);
136
+ await closeMemory();
137
+ }
138
+ function asRequest(m) {
139
+ return m && typeof m === 'object' && !Array.isArray(m) ? m : {};
140
+ }
141
+ const saveInput = z.object({
142
+ text: z.string().min(1).describe('What to remember, written to be understood on its own.'),
143
+ title: z.string().max(200).optional().describe('A short title (default: the start of the text).'),
144
+ });
145
+ function toolDefinitions(readOnly) {
146
+ // The agent tools' names, texts and schemas, from a memory that is never searched: listing needs no open memory.
147
+ const shapes = toolShapes();
148
+ const defs = shapes.map((t) => ({
149
+ name: t.name,
150
+ description: t.description,
151
+ inputSchema: t.jsonSchema,
152
+ annotations: READ_ONLY,
153
+ run: (m, args) => {
154
+ const live = m.tools().find((x) => x.name === t.name);
155
+ if (!live)
156
+ return Promise.resolve({ content: [{ type: 'text', text: `Unknown tool: ${t.name}` }], isError: true });
157
+ return live.handler(args);
158
+ },
159
+ }));
160
+ if (!readOnly) {
161
+ defs.push({
162
+ name: 'memory_save',
163
+ description: SAVE_DESCRIPTION,
164
+ inputSchema: z.toJSONSchema(saveInput),
165
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
166
+ run: async (m, args) => {
167
+ const got = saveInput.safeParse(args ?? {});
168
+ if (!got.success) {
169
+ return { content: [{ type: 'text', text: `Invalid arguments: ${got.error.issues.map((i) => `${i.path.join('.') || 'input'}: ${i.message}`).join('; ')}` }], isError: true };
170
+ }
171
+ const now = new Date();
172
+ const id = `${now.toISOString()}-${randomBytes(4).toString('hex')}`;
173
+ await m.add({ id, source: 'mcp', kind: 'note', title: got.data.title, text: got.data.text, at: now.toISOString() });
174
+ return { content: [{ type: 'text', text: `Saved (${now.toISOString().slice(0, 10)}, id ${id}).` }] };
175
+ },
176
+ });
177
+ }
178
+ return defs;
179
+ }
180
+ let shapesCache = null;
181
+ function toolShapes() {
182
+ const never = () => { throw new Error('not a memory'); };
183
+ return (shapesCache ??= buildAgentTools({ search: never, open: never, explore: never }));
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrails",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "Retrieval and exploration for any text — e-mail, chat, notes, documents, news, commits, conversations. Searchable at once, on your own machine, in any language; optional consolidation into typed knowledge with a language model.",
5
5
  "keywords": [
6
6
  "retrieval",
@@ -43,6 +43,9 @@
43
43
  },
44
44
  "./package.json": "./package.json"
45
45
  },
46
+ "bin": {
47
+ "memtrails": "dist/cli.js"
48
+ },
46
49
  "files": [
47
50
  "dist",
48
51
  "README.md",
@@ -68,6 +71,7 @@
68
71
  "onnxruntime-node": "1.23.2"
69
72
  },
70
73
  "devDependencies": {
74
+ "@modelcontextprotocol/sdk": "1.32.2",
71
75
  "@types/node": "^22.10.0",
72
76
  "esbuild": "^0.25.12",
73
77
  "tsx": "^4.20.6",