mcp-simple-memory 0.1.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/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +70 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +318 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mcp-simple-memory contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# mcp-simple-memory
|
|
2
|
+
|
|
3
|
+
Persistent memory for Claude Code. **~500 lines, 1 file, zero external databases.**
|
|
4
|
+
|
|
5
|
+
Claude Code forgets everything between sessions. This MCP server gives it a local SQLite memory that persists forever.
|
|
6
|
+
|
|
7
|
+
## Why not claude-mem?
|
|
8
|
+
|
|
9
|
+
| | claude-mem | mcp-simple-memory |
|
|
10
|
+
|---|-----------|-------------------|
|
|
11
|
+
| Codebase | ~20,000 lines | ~500 lines |
|
|
12
|
+
| External DB | ChromaDB + Python | None (SQLite built-in) |
|
|
13
|
+
| Windows | Broken (as of v10) | Works |
|
|
14
|
+
| Setup | Complex | 1 command |
|
|
15
|
+
| Search | Vector only | FTS5 + optional vector |
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# In your project directory:
|
|
21
|
+
npx mcp-simple-memory init
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Restart Claude Code. Done. You now have `mem_save`, `mem_search`, `mem_get`, and `mem_list` tools.
|
|
25
|
+
|
|
26
|
+
## Optional: Semantic Search
|
|
27
|
+
|
|
28
|
+
Add a free [Gemini API key](https://aistudio.google.com/apikey) for meaning-based search:
|
|
29
|
+
|
|
30
|
+
Edit `.mcp.json`:
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"mcp-simple-memory": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "mcp-simple-memory", "serve"],
|
|
37
|
+
"env": {
|
|
38
|
+
"GEMINI_API_KEY": "your-key-here"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
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.
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
### `mem_save`
|
|
50
|
+
Save a memory.
|
|
51
|
+
|
|
52
|
+
| Param | Required | Description |
|
|
53
|
+
|-------|----------|-------------|
|
|
54
|
+
| text | Yes | Content to save |
|
|
55
|
+
| title | No | Short title (auto-generated) |
|
|
56
|
+
| project | No | Project name (default: "default") |
|
|
57
|
+
| type | No | memory, decision, error, session_summary |
|
|
58
|
+
|
|
59
|
+
### `mem_search`
|
|
60
|
+
Search memories by keyword or meaning.
|
|
61
|
+
|
|
62
|
+
| Param | Required | Description |
|
|
63
|
+
|-------|----------|-------------|
|
|
64
|
+
| query | No | Search query (omit for recent) |
|
|
65
|
+
| limit | No | Max results (default: 20) |
|
|
66
|
+
| project | No | Filter by project |
|
|
67
|
+
| mode | No | fts, vector, auto (default: auto) |
|
|
68
|
+
|
|
69
|
+
### `mem_get`
|
|
70
|
+
Fetch full details by ID.
|
|
71
|
+
|
|
72
|
+
| Param | Required | Description |
|
|
73
|
+
|-------|----------|-------------|
|
|
74
|
+
| ids | Yes | Array of memory IDs |
|
|
75
|
+
|
|
76
|
+
### `mem_list`
|
|
77
|
+
List recent memories.
|
|
78
|
+
|
|
79
|
+
| Param | Required | Description |
|
|
80
|
+
|-------|----------|-------------|
|
|
81
|
+
| limit | No | Max results (default: 20) |
|
|
82
|
+
| project | No | Filter by project |
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
```
|
|
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
|
|
93
|
+
```
|
|
94
|
+
|
|
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.
|
|
98
|
+
|
|
99
|
+
## Data Storage
|
|
100
|
+
|
|
101
|
+
All data stays local:
|
|
102
|
+
- Database: `~/.mcp-simple-memory/memory.db`
|
|
103
|
+
- Override: set `MCP_MEMORY_DIR` environment variable
|
|
104
|
+
|
|
105
|
+
## Use with CLAUDE.md
|
|
106
|
+
|
|
107
|
+
Add to your project's `CLAUDE.md` for automatic session memory:
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
## Session Memory
|
|
111
|
+
- On session start: use `mem_search` to find recent session summaries
|
|
112
|
+
- During work: save important decisions with `mem_save` (type: "decision")
|
|
113
|
+
- On session end: save a summary with `mem_save` (type: "session_summary")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI for mcp-simple-memory
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx mcp-simple-memory init — Add to .mcp.json
|
|
7
|
+
* npx mcp-simple-memory serve — Start MCP server (used by Claude Code)
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0] || "serve";
|
|
13
|
+
if (command === "init") {
|
|
14
|
+
init();
|
|
15
|
+
}
|
|
16
|
+
else if (command === "serve") {
|
|
17
|
+
// Import and run the server
|
|
18
|
+
await import("./index.js");
|
|
19
|
+
}
|
|
20
|
+
else if (command === "help" || command === "--help" || command === "-h") {
|
|
21
|
+
console.log(`
|
|
22
|
+
mcp-simple-memory — Persistent memory for Claude Code
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
init Add mcp-simple-memory to .mcp.json in current directory
|
|
26
|
+
serve Start the MCP server (default, used by Claude Code)
|
|
27
|
+
help Show this help
|
|
28
|
+
|
|
29
|
+
Environment variables:
|
|
30
|
+
GEMINI_API_KEY Enable semantic search (optional, free tier works)
|
|
31
|
+
MCP_MEMORY_DIR Custom data directory (default: ~/.mcp-simple-memory)
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.error(`Unknown command: ${command}. Run 'mcp-simple-memory help' for usage.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
function init() {
|
|
39
|
+
const mcpPath = join(process.cwd(), ".mcp.json");
|
|
40
|
+
let config = {};
|
|
41
|
+
if (existsSync(mcpPath)) {
|
|
42
|
+
try {
|
|
43
|
+
config = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error("Error: existing .mcp.json is not valid JSON");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!config.mcpServers)
|
|
51
|
+
config.mcpServers = {};
|
|
52
|
+
if (config.mcpServers["mcp-simple-memory"]) {
|
|
53
|
+
console.log("mcp-simple-memory is already configured in .mcp.json");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
config.mcpServers["mcp-simple-memory"] = {
|
|
57
|
+
command: "npx",
|
|
58
|
+
args: ["-y", "mcp-simple-memory", "serve"],
|
|
59
|
+
env: {},
|
|
60
|
+
};
|
|
61
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
62
|
+
console.log(`Added mcp-simple-memory to ${mcpPath}`);
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("Next steps:");
|
|
65
|
+
console.log(" 1. Restart Claude Code to load the new MCP server");
|
|
66
|
+
console.log(" 2. (Optional) Add GEMINI_API_KEY to .mcp.json env for semantic search");
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Example with semantic search:");
|
|
69
|
+
console.log(' "env": { "GEMINI_API_KEY": "your-key-here" }');
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-simple-memory — Persistent memory for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* ~450 lines. SQLite (sql.js WASM, zero native deps).
|
|
6
|
+
* Optional Gemini semantic search. Works on Windows/Mac/Linux.
|
|
7
|
+
*
|
|
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
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-simple-memory — Persistent memory for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* ~450 lines. SQLite (sql.js WASM, zero native deps).
|
|
6
|
+
* Optional Gemini semantic search. Works on Windows/Mac/Linux.
|
|
7
|
+
*
|
|
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
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import initSqlJs from "sql.js";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
// ─── Config ────────────────────────────────────────────────────
|
|
22
|
+
const DATA_DIR = process.env.MCP_MEMORY_DIR || join(homedir(), ".mcp-simple-memory");
|
|
23
|
+
const DB_PATH = join(DATA_DIR, "memory.db");
|
|
24
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
|
|
25
|
+
const GEMINI_MODEL = "gemini-embedding-001";
|
|
26
|
+
const EMBEDDING_DIMS = 3072;
|
|
27
|
+
if (!existsSync(DATA_DIR)) {
|
|
28
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
// ─── Database ──────────────────────────────────────────────────
|
|
31
|
+
let db;
|
|
32
|
+
async function initDb() {
|
|
33
|
+
const SQL = await initSqlJs();
|
|
34
|
+
if (existsSync(DB_PATH)) {
|
|
35
|
+
db = new SQL.Database(readFileSync(DB_PATH));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
db = new SQL.Database();
|
|
39
|
+
}
|
|
40
|
+
db.run(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
title TEXT,
|
|
44
|
+
content TEXT NOT NULL,
|
|
45
|
+
type TEXT DEFAULT 'memory',
|
|
46
|
+
project TEXT DEFAULT 'default',
|
|
47
|
+
created_at INTEGER NOT NULL,
|
|
48
|
+
created_iso TEXT NOT NULL
|
|
49
|
+
);
|
|
50
|
+
`);
|
|
51
|
+
db.run(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
53
|
+
memory_id INTEGER PRIMARY KEY,
|
|
54
|
+
vector BLOB NOT NULL
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project);`);
|
|
58
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_mem_created ON memories(created_at);`);
|
|
59
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_mem_type ON memories(type);`);
|
|
60
|
+
persist();
|
|
61
|
+
}
|
|
62
|
+
function persist() {
|
|
63
|
+
writeFileSync(DB_PATH, Buffer.from(db.export()));
|
|
64
|
+
}
|
|
65
|
+
// ─── Query helper ──────────────────────────────────────────────
|
|
66
|
+
function queryAll(sql, params = []) {
|
|
67
|
+
const stmt = db.prepare(sql);
|
|
68
|
+
if (params.length)
|
|
69
|
+
stmt.bind(params);
|
|
70
|
+
const rows = [];
|
|
71
|
+
while (stmt.step())
|
|
72
|
+
rows.push(stmt.getAsObject());
|
|
73
|
+
stmt.free();
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
function getByIds(ids) {
|
|
77
|
+
if (!ids.length)
|
|
78
|
+
return [];
|
|
79
|
+
return queryAll(`SELECT * FROM memories WHERE id IN (${ids.map(() => "?").join(",")})`, ids);
|
|
80
|
+
}
|
|
81
|
+
// ─── Gemini Embeddings ─────────────────────────────────────────
|
|
82
|
+
async function getEmbedding(text) {
|
|
83
|
+
if (!GEMINI_API_KEY)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:embedContent?key=${GEMINI_API_KEY}`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model: `models/${GEMINI_MODEL}`,
|
|
91
|
+
content: { parts: [{ text }] },
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok)
|
|
95
|
+
return null;
|
|
96
|
+
const data = (await res.json());
|
|
97
|
+
return new Float32Array(data.embedding.values);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function cosine(a, b) {
|
|
104
|
+
let dot = 0, nA = 0, nB = 0;
|
|
105
|
+
for (let i = 0; i < a.length; i++) {
|
|
106
|
+
dot += a[i] * b[i];
|
|
107
|
+
nA += a[i] * a[i];
|
|
108
|
+
nB += b[i] * b[i];
|
|
109
|
+
}
|
|
110
|
+
return dot / (Math.sqrt(nA) * Math.sqrt(nB));
|
|
111
|
+
}
|
|
112
|
+
async function vectorSearch(query, limit, project) {
|
|
113
|
+
const qVec = await getEmbedding(query);
|
|
114
|
+
if (!qVec)
|
|
115
|
+
return [];
|
|
116
|
+
const sql = project
|
|
117
|
+
? `SELECT e.memory_id, e.vector FROM embeddings e JOIN memories m ON m.id = e.memory_id WHERE m.project = ?`
|
|
118
|
+
: `SELECT memory_id, vector FROM embeddings`;
|
|
119
|
+
const rows = queryAll(sql, project ? [project] : []);
|
|
120
|
+
const scored = rows.map((r) => {
|
|
121
|
+
const buf = r.vector instanceof Uint8Array ? r.vector : new Uint8Array(r.vector);
|
|
122
|
+
const vec = new Float32Array(buf.buffer, buf.byteOffset, EMBEDDING_DIMS);
|
|
123
|
+
return { id: r.memory_id, score: cosine(qVec, vec) };
|
|
124
|
+
});
|
|
125
|
+
scored.sort((a, b) => b.score - a.score);
|
|
126
|
+
return scored.slice(0, limit);
|
|
127
|
+
}
|
|
128
|
+
// ─── Response helpers ──────────────────────────────────────────
|
|
129
|
+
function ok(text) {
|
|
130
|
+
return { content: [{ type: "text", text }] };
|
|
131
|
+
}
|
|
132
|
+
function err(text) {
|
|
133
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
134
|
+
}
|
|
135
|
+
function formatRows(rows, header) {
|
|
136
|
+
if (!rows.length)
|
|
137
|
+
return ok(`${header}\n\n(no results)`);
|
|
138
|
+
const lines = rows.map((r) => {
|
|
139
|
+
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}`;
|
|
141
|
+
});
|
|
142
|
+
return ok(`# ${header} (${rows.length})\n\n${lines.join("\n\n")}`);
|
|
143
|
+
}
|
|
144
|
+
// ─── Tool Handlers ─────────────────────────────────────────────
|
|
145
|
+
async function handleSave(args) {
|
|
146
|
+
const content = args.text || args.content;
|
|
147
|
+
if (!content)
|
|
148
|
+
return err("text is required");
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const title = args.title || content.substring(0, 80);
|
|
151
|
+
const project = args.project || "default";
|
|
152
|
+
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()]);
|
|
154
|
+
const id = queryAll(`SELECT last_insert_rowid() as id`)[0]?.id ?? 0;
|
|
155
|
+
persist();
|
|
156
|
+
// Background embedding
|
|
157
|
+
if (GEMINI_API_KEY) {
|
|
158
|
+
getEmbedding(`${title}\n${content}`).then((vec) => {
|
|
159
|
+
if (vec) {
|
|
160
|
+
db.run(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [
|
|
161
|
+
id,
|
|
162
|
+
vec.buffer,
|
|
163
|
+
]);
|
|
164
|
+
persist();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return ok(`Saved memory #${id} (project: ${project})`);
|
|
169
|
+
}
|
|
170
|
+
async function handleSearch(args) {
|
|
171
|
+
const query = args.query;
|
|
172
|
+
const limit = args.limit || 20;
|
|
173
|
+
const project = args.project;
|
|
174
|
+
const mode = args.mode || "auto";
|
|
175
|
+
// No query → return recent
|
|
176
|
+
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");
|
|
181
|
+
}
|
|
182
|
+
let kwResults = [];
|
|
183
|
+
let vecResults = [];
|
|
184
|
+
// Keyword search (LIKE)
|
|
185
|
+
if (mode === "fts" || mode === "keyword" || mode === "auto") {
|
|
186
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
187
|
+
if (words.length) {
|
|
188
|
+
const conds = words.map(() => `(title LIKE ? OR content LIKE ?)`).join(" OR ");
|
|
189
|
+
const params = [];
|
|
190
|
+
for (const w of words) {
|
|
191
|
+
params.push(`%${w}%`, `%${w}%`);
|
|
192
|
+
}
|
|
193
|
+
let sql = `SELECT * FROM memories WHERE (${conds})`;
|
|
194
|
+
if (project) {
|
|
195
|
+
sql += ` AND project = ?`;
|
|
196
|
+
params.push(project);
|
|
197
|
+
}
|
|
198
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
199
|
+
params.push(limit);
|
|
200
|
+
kwResults = queryAll(sql, params);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Vector search (if keyword found few results)
|
|
204
|
+
if ((mode === "vector" || (mode === "auto" && kwResults.length < 3)) && GEMINI_API_KEY) {
|
|
205
|
+
vecResults = await vectorSearch(query, limit, project);
|
|
206
|
+
}
|
|
207
|
+
// Merge
|
|
208
|
+
if (vecResults.length && kwResults.length) {
|
|
209
|
+
const kwIds = new Set(kwResults.map((r) => r.id));
|
|
210
|
+
const extra = vecResults.filter((r) => !kwIds.has(r.id)).map((r) => r.id);
|
|
211
|
+
if (extra.length)
|
|
212
|
+
kwResults = [...kwResults, ...getByIds(extra)];
|
|
213
|
+
return formatRows(kwResults.slice(0, limit), `Search: "${query}" (Keyword+Vector)`);
|
|
214
|
+
}
|
|
215
|
+
if (vecResults.length) {
|
|
216
|
+
return formatRows(getByIds(vecResults.map((r) => r.id)).slice(0, limit), `Search: "${query}" (Vector)`);
|
|
217
|
+
}
|
|
218
|
+
return formatRows(kwResults.slice(0, limit), `Search: "${query}" (Keyword)`);
|
|
219
|
+
}
|
|
220
|
+
async function handleGet(args) {
|
|
221
|
+
const ids = args.ids;
|
|
222
|
+
if (!ids?.length)
|
|
223
|
+
return err("ids array is required");
|
|
224
|
+
return formatRows(getByIds(ids), `Fetched ${ids.length} memories`);
|
|
225
|
+
}
|
|
226
|
+
async function handleList(args) {
|
|
227
|
+
const limit = args.limit || 20;
|
|
228
|
+
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");
|
|
233
|
+
}
|
|
234
|
+
// ─── MCP Server ────────────────────────────────────────────────
|
|
235
|
+
const server = new Server({ name: "mcp-simple-memory", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
236
|
+
const tools = [
|
|
237
|
+
{
|
|
238
|
+
name: "mem_save",
|
|
239
|
+
description: "Save a memory. Params: text (required), title, project, type (memory/decision/error/session_summary)",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
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" },
|
|
247
|
+
},
|
|
248
|
+
required: ["text"],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "mem_search",
|
|
253
|
+
description: "Search memories by keyword or meaning. Params: query, limit, project, mode (keyword/vector/auto)",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
query: { type: "string", description: "Search query (omit for recent)" },
|
|
258
|
+
limit: { type: "number", description: "Max results (default: 20)" },
|
|
259
|
+
project: { type: "string", description: "Filter by project" },
|
|
260
|
+
mode: { type: "string", description: "keyword, vector, or auto (default: auto)" },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "mem_get",
|
|
266
|
+
description: "Fetch full details for specific memory IDs.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
ids: { type: "array", items: { type: "number" }, description: "Memory IDs to fetch" },
|
|
271
|
+
},
|
|
272
|
+
required: ["ids"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "mem_list",
|
|
277
|
+
description: "List recent memories. Params: limit, project",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
limit: { type: "number", description: "Max results (default: 20)" },
|
|
282
|
+
project: { type: "string", description: "Filter by project" },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
const handlers = {
|
|
288
|
+
mem_save: handleSave,
|
|
289
|
+
mem_search: handleSearch,
|
|
290
|
+
mem_get: handleGet,
|
|
291
|
+
mem_list: handleList,
|
|
292
|
+
};
|
|
293
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
294
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
295
|
+
}));
|
|
296
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
297
|
+
const handler = handlers[request.params.name];
|
|
298
|
+
if (!handler)
|
|
299
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
300
|
+
try {
|
|
301
|
+
return await handler(request.params.arguments || {});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return err(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
// ─── Start ─────────────────────────────────────────────────────
|
|
308
|
+
console.log = console.error; // MCP uses stdout for JSON-RPC
|
|
309
|
+
async function main() {
|
|
310
|
+
await initDb();
|
|
311
|
+
const transport = new StdioServerTransport();
|
|
312
|
+
await server.connect(transport);
|
|
313
|
+
console.error(`[mcp-simple-memory] DB: ${DB_PATH} | Embeddings: ${GEMINI_API_KEY ? "ON" : "OFF"}`);
|
|
314
|
+
}
|
|
315
|
+
main().catch((e) => {
|
|
316
|
+
console.error(`[mcp-simple-memory] Fatal: ${e}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-simple-memory": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"memory",
|
|
20
|
+
"sqlite",
|
|
21
|
+
"persistent-memory",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"ai-tools"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
29
|
+
"sql.js": "^1.13.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.15.0",
|
|
33
|
+
"@types/sql.js": "^1.4.9",
|
|
34
|
+
"typescript": "^5.8.3"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/ohsim713-cmd/mcp-simple-memory"
|
|
47
|
+
}
|
|
48
|
+
}
|