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 +79 -21
- package/dist/index.d.ts +9 -6
- package/dist/index.js +362 -52
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
# mcp-simple-memory
|
|
2
2
|
|
|
3
|
-
Persistent memory for Claude Code.
|
|
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
|
|
7
|
+
## Why This Exists
|
|
8
8
|
|
|
9
9
|
| | claude-mem | mcp-simple-memory |
|
|
10
10
|
|---|-----------|-------------------|
|
|
11
|
-
| Codebase | ~20,000 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 |
|
|
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`,
|
|
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,
|
|
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
|
|
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 |
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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. **
|
|
96
|
-
2. **Gemini Embeddings** (optional):
|
|
97
|
-
3. **Auto mode**:
|
|
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
|
|
3
|
+
* mcp-simple-memory - Persistent memory for Claude Code
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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
|
|
10
|
-
* mem_search
|
|
11
|
-
* mem_get
|
|
12
|
-
* mem_list
|
|
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
|
|
3
|
+
* mcp-simple-memory - Persistent memory for Claude Code
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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
|
|
10
|
-
* mem_search
|
|
11
|
-
* mem_get
|
|
12
|
-
* mem_list
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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)) &&
|
|
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
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
//
|
|
235
|
-
const server = new Server({ name: "mcp-simple-memory", version: "0.
|
|
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
|
|
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: {
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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.
|
|
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) => ({
|
|
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
|
-
//
|
|
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.
|
|
4
|
-
"description": "
|
|
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": {
|