memtrails 0.4.2 → 0.5.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.
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.0] — 2026-11-05
9
+
10
+ - An MCP server and a command line: `npx memtrails mcp` serves a memory to Claude Desktop, Claude Code, Cursor and any
11
+ MCP client (`memory_search`, `memory_open`, `memory_explore`, and `memory_save` unless `--read-only`), opening it at
12
+ the first tool call and closing it when idle; `memtrails add` fills it from folders, mail, JSONL, a Slack export or
13
+ git; `memtrails search` asks it. `serveMcp` serves one from code. The same in Python (`uvx memtrails mcp`,
14
+ `serve_mcp`), held to the same answers (`conformance/fixtures/mcp.json`). The Rust version has the same library at
15
+ this version, without the server and command line for now: its `Memory::tools()` serve an MCP server of your own.
16
+
17
+ ## [0.4.3] — 2026-11-05
18
+
19
+ - Fixes: the first start no longer fails with "Qdrant exited (101) while starting" where the system has no CA
20
+ certificates, as in the official slim Node image. Qdrant will not start without them, though it makes no outside
21
+ call here: it now gets the host's `SSL_CERT_FILE`, or the certificates the library carries. All three languages.
22
+
8
23
  ## [0.4.2] — 2026-11-05
9
24
 
10
25
  - Fixes: the first start no longer fails when run as root (as in most containers) with "is not this user's alone":
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,32 @@ 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
+ Fill the memory from the command line, and try it:
87
+
88
+ ```bash
89
+ npx memtrails add ~/notes ~/mail/archive.mbox ./slack-export # notes and documents, mail, a Slack export
90
+ npx memtrails add --git ~/code/my-repo # commit messages and changed files
91
+ npx memtrails search "why did the certificate stop renewing?"
92
+ ```
93
+
94
+ The memory lives in `~/.memtrails` (`--home` or `MEMTRAILS_HOME` for another). The server opens it at the first tool
95
+ call and closes it after five minutes without one (`--idle`), so `add` can run in between; `--read-only` leaves out
96
+ `memory_save`. From code: `serveMcp({ open: () => openMemory({ home }), version })`.
97
+
72
98
  ## Adding data
73
99
 
74
100
  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
+ });
@@ -4,6 +4,7 @@ import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, renameSync, rm
4
4
  import { createServer } from 'node:net';
5
5
  import { uptime } from 'node:os';
6
6
  import { join, resolve } from 'node:path';
7
+ import { rootCertificates } from 'node:tls';
7
8
  /** A port the OS says is free on loopback. */
8
9
  async function freePort() {
9
10
  return new Promise((resolve, reject) => {
@@ -95,13 +96,15 @@ function commandOf(pid) {
95
96
  }
96
97
  }
97
98
  /** What Qdrant gets of the environment: what a process needs to run and the key. None of the host's secrets, and none
98
- * of its QDRANT__* variables — they override the config (QDRANT__SERVICE__HOST=0.0.0.0 would open it to the network). */
99
- function childEnv(apiKey) {
99
+ * of its QDRANT__* variables — they override the config (QDRANT__SERVICE__HOST=0.0.0.0 would open it to the network).
100
+ * Qdrant does not start without CA certificates, though it makes no outside call here, and a slim container has none:
101
+ * it gets the host's SSL_CERT_FILE, or `caFile`. */
102
+ function childEnv(apiKey, caFile) {
100
103
  const env = {};
101
104
  for (const k of ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'SystemRoot', 'USERPROFILE'])
102
105
  if (process.env[k] !== undefined)
103
106
  env[k] = process.env[k];
104
- return { ...env, QDRANT__SERVICE__API_KEY: apiKey, QDRANT__TELEMETRY_DISABLED: 'true' };
107
+ return { ...env, SSL_CERT_FILE: process.env.SSL_CERT_FILE || caFile, QDRANT__SERVICE__API_KEY: apiKey, QDRANT__TELEMETRY_DISABLED: 'true' };
105
108
  }
106
109
  /** Every Qdrant this process started and has not seen exit: stopped when the host exits without `close()` (an 'exit'
107
110
  * handler can only send the signal; a host killed outright leaves its Qdrant to the next start's reapLeftover). */
@@ -156,13 +159,15 @@ export class QdrantProcess {
156
159
  const apiKey = randomBytes(24).toString('hex');
157
160
  const config = join(this.opts.dir, 'config.yaml');
158
161
  writeFileSync(config, qdrantConfig(this.opts.dir, port), { mode: 0o600 });
162
+ const caFile = join(this.opts.dir, 'ca.pem');
163
+ writeFileSync(caFile, `${rootCertificates.join('\n')}\n`, { mode: 0o600 }); // the ones Node carries
159
164
  const fd = openLog(this.opts.dir);
160
165
  let child;
161
166
  try {
162
167
  child = spawn(this.opts.binary, ['--config-path', config, '--disable-telemetry'], {
163
168
  cwd: this.opts.dir,
164
169
  stdio: ['ignore', fd, fd],
165
- env: childEnv(apiKey),
170
+ env: childEnv(apiKey, caFile),
166
171
  });
167
172
  }
168
173
  finally {
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.2",
3
+ "version": "0.5.0",
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",