mcp-simple-memory 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,27 +1,26 @@
1
1
  # mcp-simple-memory
2
2
 
3
- Persistent memory for Claude Code. **~500 lines, 1 file, zero external databases.**
3
+ Persistent memory for Claude Code. **One file, zero external databases.**
4
4
 
5
5
  Claude Code forgets everything between sessions. This MCP server gives it a local SQLite memory that persists forever.
6
6
 
7
- ## Why not claude-mem?
7
+ ## Why This Exists
8
8
 
9
9
  | | claude-mem | mcp-simple-memory |
10
10
  |---|-----------|-------------------|
11
- | Codebase | ~20,000 lines | ~500 lines |
11
+ | Codebase | ~20,000 lines | ~600 lines |
12
12
  | External DB | ChromaDB + Python | None (SQLite built-in) |
13
13
  | Windows | Broken (as of v10) | Works |
14
14
  | Setup | Complex | 1 command |
15
- | Search | Vector only | FTS5 + optional vector |
15
+ | Search | Vector only | Keyword + optional vector |
16
16
 
17
17
  ## Quick Start
18
18
 
19
19
  ```bash
20
- # In your project directory:
21
20
  npx mcp-simple-memory init
22
21
  ```
23
22
 
24
- Restart Claude Code. Done. You now have `mem_save`, `mem_search`, `mem_get`, and `mem_list` tools.
23
+ Restart Claude Code. Done. You now have 7 tools: `mem_save`, `mem_search`, `mem_get`, `mem_list`, `mem_update`, `mem_delete`, `mem_tags`.
25
24
 
26
25
  ## Optional: Semantic Search
27
26
 
@@ -42,29 +41,40 @@ Edit `.mcp.json`:
42
41
  }
43
42
  ```
44
43
 
45
- Without a key, only FTS5 keyword search is used. With a key, the server automatically falls back to vector search when keyword search returns few results.
44
+ Without a key, keyword search is used. With a key, vector search kicks in automatically when keyword results are sparse.
46
45
 
47
46
  ## Tools
48
47
 
49
48
  ### `mem_save`
50
- Save a memory.
49
+ Save a memory with optional tags.
50
+
51
+ ```
52
+ mem_save({ text: "OAuth2 tokens expire in 1 hour", tags: ["auth", "api"] })
53
+ ```
51
54
 
52
55
  | Param | Required | Description |
53
56
  |-------|----------|-------------|
54
57
  | text | Yes | Content to save |
55
58
  | title | No | Short title (auto-generated) |
56
59
  | project | No | Project name (default: "default") |
57
- | type | No | memory, decision, error, session_summary |
60
+ | type | No | memory, decision, error, session_summary, todo, snippet |
61
+ | tags | No | Array of tags for categorization |
58
62
 
59
63
  ### `mem_search`
60
- Search memories by keyword or meaning.
64
+ Search by keyword, meaning, tag, or type.
65
+
66
+ ```
67
+ mem_search({ query: "authentication", tag: "api" })
68
+ ```
61
69
 
62
70
  | Param | Required | Description |
63
71
  |-------|----------|-------------|
64
72
  | query | No | Search query (omit for recent) |
65
73
  | limit | No | Max results (default: 20) |
66
74
  | project | No | Filter by project |
67
- | mode | No | fts, vector, auto (default: auto) |
75
+ | mode | No | keyword, vector, auto (default: auto) |
76
+ | tag | No | Filter by tag |
77
+ | type | No | Filter by type |
68
78
 
69
79
  ### `mem_get`
70
80
  Fetch full details by ID.
@@ -74,27 +84,70 @@ Fetch full details by ID.
74
84
  | ids | Yes | Array of memory IDs |
75
85
 
76
86
  ### `mem_list`
77
- List recent memories.
87
+ List recent memories with optional filters.
78
88
 
79
89
  | Param | Required | Description |
80
90
  |-------|----------|-------------|
81
91
  | limit | No | Max results (default: 20) |
82
92
  | project | No | Filter by project |
93
+ | type | No | Filter by type |
94
+ | tag | No | Filter by tag |
95
+
96
+ ### `mem_update`
97
+ Update an existing memory's content, title, type, project, or tags.
98
+
99
+ ```
100
+ mem_update({ id: 42, text: "Updated content", tags: ["new-tag"] })
101
+ ```
102
+
103
+ | Param | Required | Description |
104
+ |-------|----------|-------------|
105
+ | id | Yes | Memory ID to update |
106
+ | text | No | New content |
107
+ | title | No | New title |
108
+ | type | No | New type |
109
+ | project | No | New project |
110
+ | tags | No | Replace all tags (pass [] to clear) |
111
+
112
+ ### `mem_delete`
113
+ Delete memories by IDs. Also removes embeddings and tags.
114
+
115
+ ```
116
+ mem_delete({ ids: [1, 2, 3] })
117
+ ```
118
+
119
+ | Param | Required | Description |
120
+ |-------|----------|-------------|
121
+ | ids | Yes | Array of memory IDs to delete |
122
+
123
+ ### `mem_tags`
124
+ List all tags with usage counts.
125
+
126
+ ```
127
+ mem_tags({ project: "my-app" })
128
+ ```
129
+
130
+ | Param | Required | Description |
131
+ |-------|----------|-------------|
132
+ | project | No | Filter by project |
83
133
 
84
134
  ## How It Works
85
135
 
86
136
  ```
87
- mem_save("Fixed the auth bug by switching to OAuth2")
88
-
89
- SQLite (FTS5 index + optional Gemini embedding)
90
-
91
- mem_search("authentication problem")
92
- → Finds it via FTS keyword match OR vector similarity
137
+ mem_save({ text: "Fixed auth bug with OAuth2", tags: ["bug", "auth"] })
138
+ |
139
+ v
140
+ SQLite (keyword index + optional Gemini embedding + tags)
141
+ |
142
+ v
143
+ mem_search({ query: "authentication problem", tag: "bug" })
144
+ -> Finds it via keyword match OR vector similarity, filtered by tag
93
145
  ```
94
146
 
95
- 1. **FTS5**: SQLite full-text search. Fast, zero-config, keyword matching.
96
- 2. **Gemini Embeddings** (optional): Converts text to 3072-dim vectors for semantic similarity. Free tier = 1500 requests/day.
97
- 3. **Auto mode**: Tries FTS5 first. If < 3 results, falls back to vector search.
147
+ 1. **Keyword Search**: SQLite LIKE queries. Fast, zero-config.
148
+ 2. **Gemini Embeddings** (optional): 3072-dim vectors for semantic similarity. Free tier = 1500 req/day.
149
+ 3. **Auto mode**: Keyword first. If < 3 results, falls back to vector search.
150
+ 4. **Tags**: Lightweight categorization. Normalized to lowercase.
98
151
 
99
152
  ## Data Storage
100
153
 
@@ -111,8 +164,13 @@ Add to your project's `CLAUDE.md` for automatic session memory:
111
164
  - On session start: use `mem_search` to find recent session summaries
112
165
  - During work: save important decisions with `mem_save` (type: "decision")
113
166
  - On session end: save a summary with `mem_save` (type: "session_summary")
167
+ - Tag memories for easy retrieval: `mem_save({ text: "...", tags: ["auth", "v2"] })`
114
168
  ```
115
169
 
170
+ ## Upgrade from 0.1.x
171
+
172
+ Just update - the database schema migrates automatically. Your existing memories are preserved. New columns (`updated_at`, `updated_iso`) and the `tags` table are added on first run.
173
+
116
174
  ## License
117
175
 
118
176
  MIT
package/dist/index.d.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * mcp-simple-memory Persistent memory for Claude Code
3
+ * mcp-simple-memory - Persistent memory for Claude Code
4
4
  *
5
- * ~450 lines. SQLite (sql.js WASM, zero native deps).
5
+ * SQLite (sql.js WASM, zero native deps).
6
6
  * Optional Gemini semantic search. Works on Windows/Mac/Linux.
7
7
  *
8
8
  * Tools:
9
- * mem_save Save a memory
10
- * mem_search Search (keyword + optional vector)
11
- * mem_get Fetch by IDs
12
- * mem_list Recent memories
9
+ * mem_save - Save a memory (with optional tags)
10
+ * mem_search - Search (keyword + optional vector)
11
+ * mem_get - Fetch by IDs (full content)
12
+ * mem_list - Recent memories
13
+ * mem_update - Update an existing memory
14
+ * mem_delete - Delete memories by IDs
15
+ * mem_tags - List all tags (with counts)
13
16
  */
14
17
  export {};
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * mcp-simple-memory Persistent memory for Claude Code
3
+ * mcp-simple-memory - Persistent memory for Claude Code
4
4
  *
5
- * ~450 lines. SQLite (sql.js WASM, zero native deps).
5
+ * SQLite (sql.js WASM, zero native deps).
6
6
  * Optional Gemini semantic search. Works on Windows/Mac/Linux.
7
7
  *
8
8
  * Tools:
9
- * mem_save Save a memory
10
- * mem_search Search (keyword + optional vector)
11
- * mem_get Fetch by IDs
12
- * mem_list Recent memories
9
+ * mem_save - Save a memory (with optional tags)
10
+ * mem_search - Search (keyword + optional vector)
11
+ * mem_get - Fetch by IDs (full content)
12
+ * mem_list - Recent memories
13
+ * mem_update - Update an existing memory
14
+ * mem_delete - Delete memories by IDs
15
+ * mem_tags - List all tags (with counts)
13
16
  */
14
17
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -18,7 +21,7 @@ import initSqlJs from "sql.js";
18
21
  import { join } from "path";
19
22
  import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
20
23
  import { homedir } from "os";
21
- // ─── Config ────────────────────────────────────────────────────
24
+ // ---- Config ---------------------------------------------------------------
22
25
  const DATA_DIR = process.env.MCP_MEMORY_DIR || join(homedir(), ".mcp-simple-memory");
23
26
  const DB_PATH = join(DATA_DIR, "memory.db");
24
27
  const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
@@ -27,7 +30,7 @@ const EMBEDDING_DIMS = 3072;
27
30
  if (!existsSync(DATA_DIR)) {
28
31
  mkdirSync(DATA_DIR, { recursive: true });
29
32
  }
30
- // ─── Database ──────────────────────────────────────────────────
33
+ // ---- Database -------------------------------------------------------------
31
34
  let db;
32
35
  async function initDb() {
33
36
  const SQL = await initSqlJs();
@@ -45,7 +48,9 @@ async function initDb() {
45
48
  type TEXT DEFAULT 'memory',
46
49
  project TEXT DEFAULT 'default',
47
50
  created_at INTEGER NOT NULL,
48
- created_iso TEXT NOT NULL
51
+ created_iso TEXT NOT NULL,
52
+ updated_at INTEGER,
53
+ updated_iso TEXT
49
54
  );
50
55
  `);
51
56
  db.run(`
@@ -53,16 +58,40 @@ async function initDb() {
53
58
  memory_id INTEGER PRIMARY KEY,
54
59
  vector BLOB NOT NULL
55
60
  );
61
+ `);
62
+ db.run(`
63
+ CREATE TABLE IF NOT EXISTS tags (
64
+ memory_id INTEGER NOT NULL,
65
+ tag TEXT NOT NULL,
66
+ PRIMARY KEY (memory_id, tag)
67
+ );
56
68
  `);
57
69
  db.run(`CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project);`);
58
70
  db.run(`CREATE INDEX IF NOT EXISTS idx_mem_created ON memories(created_at);`);
59
71
  db.run(`CREATE INDEX IF NOT EXISTS idx_mem_type ON memories(type);`);
72
+ db.run(`CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);`);
73
+ // Migration: add updated_at/updated_iso columns if missing
74
+ try {
75
+ db.run(`SELECT updated_at FROM memories LIMIT 1`);
76
+ }
77
+ catch {
78
+ try {
79
+ db.run(`ALTER TABLE memories ADD COLUMN updated_at INTEGER`);
80
+ }
81
+ catch { /* already exists */ }
82
+ try {
83
+ db.run(`ALTER TABLE memories ADD COLUMN updated_iso TEXT`);
84
+ }
85
+ catch { /* already exists */ }
86
+ }
87
+ // Migration: create tags table if missing (for upgrades from 0.1.x)
88
+ // Already handled by CREATE TABLE IF NOT EXISTS above
60
89
  persist();
61
90
  }
62
91
  function persist() {
63
92
  writeFileSync(DB_PATH, Buffer.from(db.export()));
64
93
  }
65
- // ─── Query helper ──────────────────────────────────────────────
94
+ // ---- Query helper ---------------------------------------------------------
66
95
  function queryAll(sql, params = []) {
67
96
  const stmt = db.prepare(sql);
68
97
  if (params.length)
@@ -73,12 +102,35 @@ function queryAll(sql, params = []) {
73
102
  stmt.free();
74
103
  return rows;
75
104
  }
105
+ function runSql(sql, params = []) {
106
+ db.run(sql, params);
107
+ }
76
108
  function getByIds(ids) {
77
109
  if (!ids.length)
78
110
  return [];
79
- return queryAll(`SELECT * FROM memories WHERE id IN (${ids.map(() => "?").join(",")})`, ids);
111
+ const rows = queryAll(`SELECT * FROM memories WHERE id IN (${ids.map(() => "?").join(",")})`, ids);
112
+ // Attach tags to each row
113
+ for (const row of rows) {
114
+ row.tags = getTagsForMemory(row.id);
115
+ }
116
+ return rows;
117
+ }
118
+ function getTagsForMemory(memoryId) {
119
+ return queryAll(`SELECT tag FROM tags WHERE memory_id = ?`, [memoryId]).map((r) => r.tag);
120
+ }
121
+ function setTagsForMemory(memoryId, tags) {
122
+ runSql(`DELETE FROM tags WHERE memory_id = ?`, [memoryId]);
123
+ for (const tag of tags) {
124
+ const normalized = tag.trim().toLowerCase();
125
+ if (normalized) {
126
+ runSql(`INSERT OR IGNORE INTO tags (memory_id, tag) VALUES (?, ?)`, [
127
+ memoryId,
128
+ normalized,
129
+ ]);
130
+ }
131
+ }
80
132
  }
81
- // ─── Gemini Embeddings ─────────────────────────────────────────
133
+ // ---- Gemini Embeddings ----------------------------------------------------
82
134
  async function getEmbedding(text) {
83
135
  if (!GEMINI_API_KEY)
84
136
  return null;
@@ -107,7 +159,8 @@ function cosine(a, b) {
107
159
  nA += a[i] * a[i];
108
160
  nB += b[i] * b[i];
109
161
  }
110
- return dot / (Math.sqrt(nA) * Math.sqrt(nB));
162
+ const denom = Math.sqrt(nA) * Math.sqrt(nB);
163
+ return denom === 0 ? 0 : dot / denom;
111
164
  }
112
165
  async function vectorSearch(query, limit, project) {
113
166
  const qVec = await getEmbedding(query);
@@ -125,7 +178,7 @@ async function vectorSearch(query, limit, project) {
125
178
  scored.sort((a, b) => b.score - a.score);
126
179
  return scored.slice(0, limit);
127
180
  }
128
- // ─── Response helpers ──────────────────────────────────────────
181
+ // ---- Response helpers -----------------------------------------------------
129
182
  function ok(text) {
130
183
  return { content: [{ type: "text", text }] };
131
184
  }
@@ -137,11 +190,13 @@ function formatRows(rows, header) {
137
190
  return ok(`${header}\n\n(no results)`);
138
191
  const lines = rows.map((r) => {
139
192
  const preview = (r.content || "").substring(0, 150).replace(/\n/g, " ");
140
- return `#${r.id} | ${r.type} | ${r.project} | ${r.created_iso}\n ${r.title || "(no title)"}\n ${preview}`;
193
+ const tags = r.tags?.length ? ` [${r.tags.join(", ")}]` : "";
194
+ const updated = r.updated_iso ? ` (updated: ${r.updated_iso})` : "";
195
+ return `#${r.id} | ${r.type} | ${r.project} | ${r.created_iso}${updated}\n ${r.title || "(no title)"}${tags}\n ${preview}`;
141
196
  });
142
197
  return ok(`# ${header} (${rows.length})\n\n${lines.join("\n\n")}`);
143
198
  }
144
- // ─── Tool Handlers ─────────────────────────────────────────────
199
+ // ---- Tool Handlers --------------------------------------------------------
145
200
  async function handleSave(args) {
146
201
  const content = args.text || args.content;
147
202
  if (!content)
@@ -150,34 +205,73 @@ async function handleSave(args) {
150
205
  const title = args.title || content.substring(0, 80);
151
206
  const project = args.project || "default";
152
207
  const type = args.type || "memory";
153
- db.run(`INSERT INTO memories (title, content, type, project, created_at, created_iso) VALUES (?, ?, ?, ?, ?, ?)`, [title, content, type, project, now, new Date(now).toISOString()]);
208
+ runSql(`INSERT INTO memories (title, content, type, project, created_at, created_iso) VALUES (?, ?, ?, ?, ?, ?)`, [title, content, type, project, now, new Date(now).toISOString()]);
154
209
  const id = queryAll(`SELECT last_insert_rowid() as id`)[0]?.id ?? 0;
210
+ // Tags
211
+ const tags = args.tags || [];
212
+ if (tags.length) {
213
+ setTagsForMemory(id, tags);
214
+ }
155
215
  persist();
156
- // Background embedding
216
+ // Background embedding (fire-and-forget)
157
217
  if (GEMINI_API_KEY) {
158
218
  getEmbedding(`${title}\n${content}`).then((vec) => {
159
219
  if (vec) {
160
- db.run(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [
161
- id,
162
- vec.buffer,
163
- ]);
220
+ runSql(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [id, vec.buffer]);
164
221
  persist();
165
222
  }
166
223
  });
167
224
  }
168
- return ok(`Saved memory #${id} (project: ${project})`);
225
+ const tagStr = tags.length ? ` tags: [${tags.join(", ")}]` : "";
226
+ return ok(`Saved memory #${id} (project: ${project})${tagStr}`);
169
227
  }
170
228
  async function handleSearch(args) {
171
229
  const query = args.query;
172
230
  const limit = args.limit || 20;
173
231
  const project = args.project;
174
232
  const mode = args.mode || "auto";
175
- // No query → return recent
233
+ const tag = args.tag;
234
+ const type = args.type;
235
+ // Tag-only search
236
+ if (tag && !query) {
237
+ let sql = `SELECT m.* FROM memories m JOIN tags t ON m.id = t.memory_id WHERE t.tag = ?`;
238
+ const params = [tag.toLowerCase()];
239
+ if (project) {
240
+ sql += ` AND m.project = ?`;
241
+ params.push(project);
242
+ }
243
+ if (type) {
244
+ sql += ` AND m.type = ?`;
245
+ params.push(type);
246
+ }
247
+ sql += ` ORDER BY m.created_at DESC LIMIT ?`;
248
+ params.push(limit);
249
+ const rows = queryAll(sql, params);
250
+ for (const row of rows)
251
+ row.tags = getTagsForMemory(row.id);
252
+ return formatRows(rows, `Tag: "${tag}"`);
253
+ }
254
+ // No query -> return recent
176
255
  if (!query) {
177
- const sql = project
178
- ? `SELECT * FROM memories WHERE project = ? ORDER BY created_at DESC LIMIT ?`
179
- : `SELECT * FROM memories ORDER BY created_at DESC LIMIT ?`;
180
- return formatRows(queryAll(sql, project ? [project, limit] : [limit]), "Recent memories");
256
+ let sql = `SELECT * FROM memories`;
257
+ const params = [];
258
+ const conds = [];
259
+ if (project) {
260
+ conds.push(`project = ?`);
261
+ params.push(project);
262
+ }
263
+ if (type) {
264
+ conds.push(`type = ?`);
265
+ params.push(type);
266
+ }
267
+ if (conds.length)
268
+ sql += ` WHERE ${conds.join(" AND ")}`;
269
+ sql += ` ORDER BY created_at DESC LIMIT ?`;
270
+ params.push(limit);
271
+ const rows = queryAll(sql, params);
272
+ for (const row of rows)
273
+ row.tags = getTagsForMemory(row.id);
274
+ return formatRows(rows, "Recent memories");
181
275
  }
182
276
  let kwResults = [];
183
277
  let vecResults = [];
@@ -185,29 +279,48 @@ async function handleSearch(args) {
185
279
  if (mode === "fts" || mode === "keyword" || mode === "auto") {
186
280
  const words = query.split(/\s+/).filter((w) => w.length > 0);
187
281
  if (words.length) {
188
- const conds = words.map(() => `(title LIKE ? OR content LIKE ?)`).join(" OR ");
282
+ const wordConds = words
283
+ .map(() => `(m.title LIKE ? OR m.content LIKE ?)`)
284
+ .join(" OR ");
189
285
  const params = [];
190
286
  for (const w of words) {
191
287
  params.push(`%${w}%`, `%${w}%`);
192
288
  }
193
- let sql = `SELECT * FROM memories WHERE (${conds})`;
289
+ let sql = `SELECT DISTINCT m.* FROM memories m`;
290
+ if (tag) {
291
+ sql += ` JOIN tags t ON m.id = t.memory_id`;
292
+ }
293
+ sql += ` WHERE (${wordConds})`;
294
+ if (tag) {
295
+ sql += ` AND t.tag = ?`;
296
+ params.push(tag.toLowerCase());
297
+ }
194
298
  if (project) {
195
- sql += ` AND project = ?`;
299
+ sql += ` AND m.project = ?`;
196
300
  params.push(project);
197
301
  }
198
- sql += ` ORDER BY created_at DESC LIMIT ?`;
302
+ if (type) {
303
+ sql += ` AND m.type = ?`;
304
+ params.push(type);
305
+ }
306
+ sql += ` ORDER BY m.created_at DESC LIMIT ?`;
199
307
  params.push(limit);
200
308
  kwResults = queryAll(sql, params);
309
+ for (const row of kwResults)
310
+ row.tags = getTagsForMemory(row.id);
201
311
  }
202
312
  }
203
313
  // Vector search (if keyword found few results)
204
- if ((mode === "vector" || (mode === "auto" && kwResults.length < 3)) && GEMINI_API_KEY) {
314
+ if ((mode === "vector" || (mode === "auto" && kwResults.length < 3)) &&
315
+ GEMINI_API_KEY) {
205
316
  vecResults = await vectorSearch(query, limit, project);
206
317
  }
207
318
  // Merge
208
319
  if (vecResults.length && kwResults.length) {
209
320
  const kwIds = new Set(kwResults.map((r) => r.id));
210
- const extra = vecResults.filter((r) => !kwIds.has(r.id)).map((r) => r.id);
321
+ const extra = vecResults
322
+ .filter((r) => !kwIds.has(r.id))
323
+ .map((r) => r.id);
211
324
  if (extra.length)
212
325
  kwResults = [...kwResults, ...getByIds(extra)];
213
326
  return formatRows(kwResults.slice(0, limit), `Search: "${query}" (Keyword+Vector)`);
@@ -226,38 +339,177 @@ async function handleGet(args) {
226
339
  async function handleList(args) {
227
340
  const limit = args.limit || 20;
228
341
  const project = args.project;
229
- const sql = project
230
- ? `SELECT * FROM memories WHERE project = ? ORDER BY created_at DESC LIMIT ?`
231
- : `SELECT * FROM memories ORDER BY created_at DESC LIMIT ?`;
232
- return formatRows(queryAll(sql, project ? [project, limit] : [limit]), "Memories");
342
+ const type = args.type;
343
+ const tag = args.tag;
344
+ if (tag) {
345
+ let sql = `SELECT m.* FROM memories m JOIN tags t ON m.id = t.memory_id WHERE t.tag = ?`;
346
+ const params = [tag.toLowerCase()];
347
+ if (project) {
348
+ sql += ` AND m.project = ?`;
349
+ params.push(project);
350
+ }
351
+ if (type) {
352
+ sql += ` AND m.type = ?`;
353
+ params.push(type);
354
+ }
355
+ sql += ` ORDER BY m.created_at DESC LIMIT ?`;
356
+ params.push(limit);
357
+ const rows = queryAll(sql, params);
358
+ for (const row of rows)
359
+ row.tags = getTagsForMemory(row.id);
360
+ return formatRows(rows, `Memories (tag: ${tag})`);
361
+ }
362
+ let sql = `SELECT * FROM memories`;
363
+ const params = [];
364
+ const conds = [];
365
+ if (project) {
366
+ conds.push(`project = ?`);
367
+ params.push(project);
368
+ }
369
+ if (type) {
370
+ conds.push(`type = ?`);
371
+ params.push(type);
372
+ }
373
+ if (conds.length)
374
+ sql += ` WHERE ${conds.join(" AND ")}`;
375
+ sql += ` ORDER BY created_at DESC LIMIT ?`;
376
+ params.push(limit);
377
+ const rows = queryAll(sql, params);
378
+ for (const row of rows)
379
+ row.tags = getTagsForMemory(row.id);
380
+ return formatRows(rows, "Memories");
381
+ }
382
+ async function handleUpdate(args) {
383
+ const id = args.id;
384
+ if (!id)
385
+ return err("id is required");
386
+ const existing = getByIds([id]);
387
+ if (!existing.length)
388
+ return err(`Memory #${id} not found`);
389
+ const now = Date.now();
390
+ const updates = [];
391
+ const params = [];
392
+ if (args.text !== undefined || args.content !== undefined) {
393
+ updates.push(`content = ?`);
394
+ params.push(args.text || args.content);
395
+ }
396
+ if (args.title !== undefined) {
397
+ updates.push(`title = ?`);
398
+ params.push(args.title);
399
+ }
400
+ if (args.type !== undefined) {
401
+ updates.push(`type = ?`);
402
+ params.push(args.type);
403
+ }
404
+ if (args.project !== undefined) {
405
+ updates.push(`project = ?`);
406
+ params.push(args.project);
407
+ }
408
+ if (!updates.length && !args.tags) {
409
+ return err("Nothing to update. Provide text, title, type, project, or tags.");
410
+ }
411
+ if (updates.length) {
412
+ updates.push(`updated_at = ?`, `updated_iso = ?`);
413
+ params.push(now, new Date(now).toISOString());
414
+ params.push(id);
415
+ runSql(`UPDATE memories SET ${updates.join(", ")} WHERE id = ?`, params);
416
+ }
417
+ // Update tags if provided
418
+ if (args.tags !== undefined) {
419
+ setTagsForMemory(id, args.tags || []);
420
+ }
421
+ persist();
422
+ // Re-embed if content changed
423
+ if ((args.text || args.content) && GEMINI_API_KEY) {
424
+ const newContent = args.text || args.content;
425
+ const newTitle = args.title || existing[0].title;
426
+ getEmbedding(`${newTitle}\n${newContent}`).then((vec) => {
427
+ if (vec) {
428
+ runSql(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [id, vec.buffer]);
429
+ persist();
430
+ }
431
+ });
432
+ }
433
+ return ok(`Updated memory #${id}`);
434
+ }
435
+ async function handleDelete(args) {
436
+ const ids = args.ids;
437
+ if (!ids?.length)
438
+ return err("ids array is required");
439
+ const existing = getByIds(ids);
440
+ if (!existing.length)
441
+ return err("No matching memories found");
442
+ const placeholders = ids.map(() => "?").join(",");
443
+ runSql(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
444
+ runSql(`DELETE FROM embeddings WHERE memory_id IN (${placeholders})`, ids);
445
+ runSql(`DELETE FROM tags WHERE memory_id IN (${placeholders})`, ids);
446
+ persist();
447
+ return ok(`Deleted ${existing.length} memor${existing.length === 1 ? "y" : "ies"}: ${existing.map((r) => `#${r.id}`).join(", ")}`);
448
+ }
449
+ async function handleTags(args) {
450
+ const project = args.project;
451
+ let sql = `SELECT t.tag, COUNT(*) as count FROM tags t`;
452
+ const params = [];
453
+ if (project) {
454
+ sql += ` JOIN memories m ON m.id = t.memory_id WHERE m.project = ?`;
455
+ params.push(project);
456
+ }
457
+ sql += ` GROUP BY t.tag ORDER BY count DESC`;
458
+ const rows = queryAll(sql, params);
459
+ if (!rows.length)
460
+ return ok("No tags found.");
461
+ const lines = rows.map((r) => ` ${r.tag} (${r.count})`);
462
+ return ok(`# Tags (${rows.length})\n\n${lines.join("\n")}`);
233
463
  }
234
- // ─── MCP Server ────────────────────────────────────────────────
235
- const server = new Server({ name: "mcp-simple-memory", version: "0.1.0" }, { capabilities: { tools: {} } });
464
+ // ---- MCP Server -----------------------------------------------------------
465
+ const server = new Server({ name: "mcp-simple-memory", version: "0.2.0" }, { capabilities: { tools: {} } });
236
466
  const tools = [
237
467
  {
238
468
  name: "mem_save",
239
- description: "Save a memory. Params: text (required), title, project, type (memory/decision/error/session_summary)",
469
+ description: "Save a memory with optional tags. Params: text (required), title, project, type, tags[]",
240
470
  inputSchema: {
241
471
  type: "object",
242
472
  properties: {
243
473
  text: { type: "string", description: "Content to save" },
244
- title: { type: "string", description: "Short title (auto-generated if omitted)" },
245
- project: { type: "string", description: "Project name (default: 'default')" },
246
- type: { type: "string", description: "Type: memory, decision, error, session_summary" },
474
+ title: {
475
+ type: "string",
476
+ description: "Short title (auto-generated if omitted)",
477
+ },
478
+ project: {
479
+ type: "string",
480
+ description: "Project name (default: 'default')",
481
+ },
482
+ type: {
483
+ type: "string",
484
+ description: "Type: memory, decision, error, session_summary, todo, snippet",
485
+ },
486
+ tags: {
487
+ type: "array",
488
+ items: { type: "string" },
489
+ description: "Tags for categorization (e.g. ['bug', 'auth'])",
490
+ },
247
491
  },
248
492
  required: ["text"],
249
493
  },
250
494
  },
251
495
  {
252
496
  name: "mem_search",
253
- description: "Search memories by keyword or meaning. Params: query, limit, project, mode (keyword/vector/auto)",
497
+ description: "Search memories by keyword, meaning, tag, or type. Params: query, limit, project, mode, tag, type",
254
498
  inputSchema: {
255
499
  type: "object",
256
500
  properties: {
257
- query: { type: "string", description: "Search query (omit for recent)" },
501
+ query: {
502
+ type: "string",
503
+ description: "Search query (omit for recent)",
504
+ },
258
505
  limit: { type: "number", description: "Max results (default: 20)" },
259
506
  project: { type: "string", description: "Filter by project" },
260
- mode: { type: "string", description: "keyword, vector, or auto (default: auto)" },
507
+ mode: {
508
+ type: "string",
509
+ description: "keyword, vector, or auto (default: auto)",
510
+ },
511
+ tag: { type: "string", description: "Filter by tag" },
512
+ type: { type: "string", description: "Filter by type" },
261
513
  },
262
514
  },
263
515
  },
@@ -267,19 +519,70 @@ const tools = [
267
519
  inputSchema: {
268
520
  type: "object",
269
521
  properties: {
270
- ids: { type: "array", items: { type: "number" }, description: "Memory IDs to fetch" },
522
+ ids: {
523
+ type: "array",
524
+ items: { type: "number" },
525
+ description: "Memory IDs to fetch",
526
+ },
271
527
  },
272
528
  required: ["ids"],
273
529
  },
274
530
  },
275
531
  {
276
532
  name: "mem_list",
277
- description: "List recent memories. Params: limit, project",
533
+ description: "List recent memories. Filter by project, type, or tag.",
278
534
  inputSchema: {
279
535
  type: "object",
280
536
  properties: {
281
537
  limit: { type: "number", description: "Max results (default: 20)" },
282
538
  project: { type: "string", description: "Filter by project" },
539
+ type: { type: "string", description: "Filter by type" },
540
+ tag: { type: "string", description: "Filter by tag" },
541
+ },
542
+ },
543
+ },
544
+ {
545
+ name: "mem_update",
546
+ description: "Update an existing memory. Params: id (required), text, title, type, project, tags[]",
547
+ inputSchema: {
548
+ type: "object",
549
+ properties: {
550
+ id: { type: "number", description: "Memory ID to update" },
551
+ text: { type: "string", description: "New content" },
552
+ title: { type: "string", description: "New title" },
553
+ type: { type: "string", description: "New type" },
554
+ project: { type: "string", description: "New project" },
555
+ tags: {
556
+ type: "array",
557
+ items: { type: "string" },
558
+ description: "Replace all tags (pass [] to clear)",
559
+ },
560
+ },
561
+ required: ["id"],
562
+ },
563
+ },
564
+ {
565
+ name: "mem_delete",
566
+ description: "Delete memories by IDs. Also removes embeddings and tags.",
567
+ inputSchema: {
568
+ type: "object",
569
+ properties: {
570
+ ids: {
571
+ type: "array",
572
+ items: { type: "number" },
573
+ description: "Memory IDs to delete",
574
+ },
575
+ },
576
+ required: ["ids"],
577
+ },
578
+ },
579
+ {
580
+ name: "mem_tags",
581
+ description: "List all tags with their usage counts. Optionally filter by project.",
582
+ inputSchema: {
583
+ type: "object",
584
+ properties: {
585
+ project: { type: "string", description: "Filter by project" },
283
586
  },
284
587
  },
285
588
  },
@@ -289,9 +592,16 @@ const handlers = {
289
592
  mem_search: handleSearch,
290
593
  mem_get: handleGet,
291
594
  mem_list: handleList,
595
+ mem_update: handleUpdate,
596
+ mem_delete: handleDelete,
597
+ mem_tags: handleTags,
292
598
  };
293
599
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
294
- tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
600
+ tools: tools.map((t) => ({
601
+ name: t.name,
602
+ description: t.description,
603
+ inputSchema: t.inputSchema,
604
+ })),
295
605
  }));
296
606
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
297
607
  const handler = handlers[request.params.name];
@@ -304,13 +614,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
304
614
  return err(`Error: ${error instanceof Error ? error.message : String(error)}`);
305
615
  }
306
616
  });
307
- // ─── Start ─────────────────────────────────────────────────────
617
+ // ---- Start ----------------------------------------------------------------
308
618
  console.log = console.error; // MCP uses stdout for JSON-RPC
309
619
  async function main() {
310
620
  await initDb();
311
621
  const transport = new StdioServerTransport();
312
622
  await server.connect(transport);
313
- console.error(`[mcp-simple-memory] DB: ${DB_PATH} | Embeddings: ${GEMINI_API_KEY ? "ON" : "OFF"}`);
623
+ console.error(`[mcp-simple-memory] v0.2.0 | DB: ${DB_PATH} | Embeddings: ${GEMINI_API_KEY ? "ON" : "OFF"}`);
314
624
  }
315
625
  main().catch((e) => {
316
626
  console.error(`[mcp-simple-memory] Fatal: ${e}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-simple-memory",
3
- "version": "0.1.0",
4
- "description": "Simple persistent memory for Claude Code. ~500 lines, SQLite, optional semantic search. Zero native deps.",
3
+ "version": "0.2.0",
4
+ "description": "Persistent memory for Claude Code via MCP. SQLite + optional semantic search. Zero native deps. Works on Windows/Mac/Linux.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "dev": "tsx src/index.ts",
13
+ "test": "node --import tsx --test test/*.test.ts",
13
14
  "prepublishOnly": "npm run build"
14
15
  },
15
16
  "keywords": [
@@ -31,6 +32,7 @@
31
32
  "devDependencies": {
32
33
  "@types/node": "^22.15.0",
33
34
  "@types/sql.js": "^1.4.9",
35
+ "tsx": "^4.19.0",
34
36
  "typescript": "^5.8.3"
35
37
  },
36
38
  "engines": {