@alex900530/claude-persistent-memory 1.0.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.
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Memory MCP Server [v1.0]
4
+ *
5
+ * Pull model: exposes memory search as MCP tools, invoked by Claude on demand.
6
+ * Complements the Push model (hooks that auto-inject context).
7
+ *
8
+ * Tools:
9
+ * - memory_search: hybrid search (BM25 + vector similarity)
10
+ * - memory_save: save new memory
11
+ * - memory_validate: validate memory usefulness (adjust confidence)
12
+ * - memory_stats: view memory statistics
13
+ *
14
+ * Transport: stdio (Claude Code standard)
15
+ */
16
+
17
+ const path = require('path');
18
+
19
+ // Try standard import first, fallback to manual path resolution
20
+ let McpServer, StdioServerTransport;
21
+ try {
22
+ ({ McpServer } = require('@modelcontextprotocol/sdk/dist/cjs/server/mcp.js'));
23
+ ({ StdioServerTransport } = require('@modelcontextprotocol/sdk/dist/cjs/server/stdio.js'));
24
+ } catch (e) {
25
+ const SDK_DIR = path.join(__dirname, '..', 'node_modules', '@modelcontextprotocol', 'sdk', 'dist', 'cjs', 'server');
26
+ ({ McpServer } = require(path.join(SDK_DIR, 'mcp.js')));
27
+ ({ StdioServerTransport } = require(path.join(SDK_DIR, 'stdio.js')));
28
+ }
29
+ const z = require('zod');
30
+
31
+ // ============ Memory modules ============
32
+
33
+ const memoryDb = require('../lib/memory-db');
34
+
35
+ // Embedding client for hybrid search via embedding server
36
+ let embeddingClient;
37
+ try {
38
+ embeddingClient = require('../lib/embedding-client');
39
+ } catch (e) {
40
+ // embedding-client not available, will fall back to memoryDb.search
41
+ }
42
+
43
+ /**
44
+ * Execute hybrid search (prefer embedding service, fallback to inline search)
45
+ */
46
+ async function hybridSearch(query, limit, options = {}) {
47
+ let useEmbeddingService = false;
48
+
49
+ if (embeddingClient) {
50
+ try {
51
+ useEmbeddingService = await embeddingClient.isServerRunning();
52
+ } catch (e) {
53
+ // ignore
54
+ }
55
+ }
56
+
57
+ if (useEmbeddingService) {
58
+ return embeddingClient.search(query, limit);
59
+ }
60
+
61
+ // Fallback: inline hybrid search
62
+ try {
63
+ return await memoryDb.search(query, limit, options);
64
+ } catch (e) {
65
+ // Final fallback: pure BM25
66
+ const keywords = memoryDb.extractKeywords(query);
67
+ const ftsQuery = keywords.map(k => `"${k}"`).join(' OR ');
68
+ return memoryDb.quickSearch(ftsQuery, limit);
69
+ }
70
+ }
71
+
72
+ // ============ MCP Server definition ============
73
+
74
+ const server = new McpServer({
75
+ name: 'memory',
76
+ version: '1.0.0'
77
+ });
78
+
79
+ // --- Tool: memory_search ---
80
+ server.tool(
81
+ 'memory_search',
82
+ 'Search persistent memories (hybrid BM25 + vector semantic retrieval). Use when you need to recall previous context, patterns, decisions, or bug fix records.',
83
+ {
84
+ query: z.string().describe('Search query (natural language, supports Chinese and English)'),
85
+ limit: z.number().optional().default(5).describe('Number of results to return (default 5)'),
86
+ type: z.enum(['fact', 'decision', 'bug', 'pattern', 'context', 'preference', 'skill']).optional().describe('Filter by memory type'),
87
+ domain: z.enum(['orm', 'api', 'frontend', 'backend', 'testing', 'memory', 'general']).optional().describe('Filter by domain')
88
+ },
89
+ async ({ query, limit = 5, type, domain }) => {
90
+ try {
91
+ const options = {};
92
+ if (type) options.type = type;
93
+ if (domain) options.domain = domain;
94
+
95
+ const results = await hybridSearch(query, limit, options);
96
+
97
+ if (!results || results.length === 0) {
98
+ return {
99
+ content: [{ type: 'text', text: 'No relevant memories found.' }]
100
+ };
101
+ }
102
+
103
+ // Mark memories as used
104
+ const usedIds = results.map(r => r.id).filter(Boolean);
105
+ if (usedIds.length > 0) {
106
+ memoryDb.markMemoriesUsed(usedIds);
107
+ }
108
+
109
+ // Format results
110
+ const formatted = results.map((r, i) => {
111
+ const confidence = r.confidence ? `${Math.round(r.confidence * 100)}%` : 'N/A';
112
+ const vecSim = r.vectorSimilarity ? r.vectorSimilarity.toFixed(3) : 'N/A';
113
+ const bm25 = r.bm25Score ? r.bm25Score.toFixed(1) : '0';
114
+ const date = r.createdAt ? r.createdAt.slice(0, 10) : r.date || 'unknown';
115
+
116
+ let content = r.content || r.rawContent || '';
117
+ // Truncate overly long content
118
+ if (content.length > 500) {
119
+ content = content.slice(0, 500) + '...';
120
+ }
121
+
122
+ return [
123
+ `## Memory #${r.id} [${r.type || 'unknown'}/${r.domain || 'general'}] (confidence: ${confidence})`,
124
+ `date: ${date} | vecSim: ${vecSim} | BM25: ${bm25}`,
125
+ '',
126
+ content
127
+ ].join('\n');
128
+ });
129
+
130
+ return {
131
+ content: [{
132
+ type: 'text',
133
+ text: `Found ${results.length} relevant memories:\n\n${formatted.join('\n\n---\n\n')}`
134
+ }]
135
+ };
136
+ } catch (e) {
137
+ return {
138
+ content: [{ type: 'text', text: `Search failed: ${e.message}` }],
139
+ isError: true
140
+ };
141
+ }
142
+ }
143
+ );
144
+
145
+ // --- Tool: memory_save ---
146
+ server.tool(
147
+ 'memory_save',
148
+ 'Save a new persistent memory. Use to record important patterns, decisions, bug fixes, user preferences, etc.',
149
+ {
150
+ content: z.string().describe('Memory content to save'),
151
+ type: z.enum(['fact', 'decision', 'bug', 'pattern', 'context', 'preference']).optional().default('context').describe('Memory type'),
152
+ domain: z.enum(['orm', 'api', 'frontend', 'backend', 'testing', 'memory', 'general']).optional().default('general').describe('Domain'),
153
+ confidence: z.number().min(0.3).max(0.9).optional().default(0.7).describe('Confidence (0.3-0.9)')
154
+ },
155
+ async ({ content, type = 'context', domain = 'general', confidence = 0.7 }) => {
156
+ try {
157
+ const result = await memoryDb.save(content, {
158
+ type,
159
+ domain,
160
+ confidence,
161
+ source: 'mcp-tool'
162
+ });
163
+
164
+ return {
165
+ content: [{
166
+ type: 'text',
167
+ text: `Memory saved (ID: ${result.id}, type: ${type}, domain: ${domain}, confidence: ${confidence})`
168
+ }]
169
+ };
170
+ } catch (e) {
171
+ return {
172
+ content: [{ type: 'text', text: `Save failed: ${e.message}` }],
173
+ isError: true
174
+ };
175
+ }
176
+ }
177
+ );
178
+
179
+ // --- Tool: memory_validate ---
180
+ server.tool(
181
+ 'memory_validate',
182
+ 'Validate whether a memory was helpful. Helpful increases confidence by +0.1, unhelpful decreases by -0.05.',
183
+ {
184
+ memory_id: z.number().describe('Memory ID'),
185
+ is_valid: z.boolean().describe('Whether the memory was helpful (true=helpful, false=not helpful)')
186
+ },
187
+ async ({ memory_id, is_valid }) => {
188
+ try {
189
+ memoryDb.validateMemory(memory_id, is_valid);
190
+ const action = is_valid ? 'increased +0.1' : 'decreased -0.05';
191
+ return {
192
+ content: [{
193
+ type: 'text',
194
+ text: `Memory #${memory_id} validated, confidence ${action}`
195
+ }]
196
+ };
197
+ } catch (e) {
198
+ return {
199
+ content: [{ type: 'text', text: `Validation failed: ${e.message}` }],
200
+ isError: true
201
+ };
202
+ }
203
+ }
204
+ );
205
+
206
+ // --- Tool: memory_stats ---
207
+ server.tool(
208
+ 'memory_stats',
209
+ 'View memory system statistics: total memories, type distribution, domain distribution, cluster status, etc.',
210
+ {},
211
+ async () => {
212
+ try {
213
+ const stats = memoryDb.getStats();
214
+
215
+ const lines = [
216
+ `## Memory System Statistics`,
217
+ `- Total memories: ${stats.totalMemories}`,
218
+ `- Total clusters: ${stats.totalClusters} (mature: ${stats.matureClusters})`,
219
+ '',
220
+ '### By Type',
221
+ ...Object.entries(stats.byType).map(([k, v]) => ` - ${k}: ${v}`),
222
+ '',
223
+ '### By Domain',
224
+ ...Object.entries(stats.byDomain).map(([k, v]) => ` - ${k}: ${v}`)
225
+ ];
226
+
227
+ return {
228
+ content: [{ type: 'text', text: lines.join('\n') }]
229
+ };
230
+ } catch (e) {
231
+ return {
232
+ content: [{ type: 'text', text: `Failed to get stats: ${e.message}` }],
233
+ isError: true
234
+ };
235
+ }
236
+ }
237
+ );
238
+
239
+ // ============ Start server ============
240
+
241
+ async function main() {
242
+ const transport = new StdioServerTransport();
243
+ await server.connect(transport);
244
+ // MCP server is now running via stdio
245
+ // stderr is used for logs, does not affect MCP protocol communication
246
+ process.stderr.write('[memory-mcp] Server started\n');
247
+ }
248
+
249
+ main().catch(e => {
250
+ process.stderr.write(`[memory-mcp] Fatal: ${e.message}\n`);
251
+ process.exit(1);
252
+ });
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ const memoryDb = require('../lib/memory-db');
3
+
4
+ async function main() {
5
+ console.log('Rebuilding all embeddings...');
6
+ const vecResult = await memoryDb.rebuildAllEmbeddings();
7
+ console.log('Embeddings done:', vecResult);
8
+
9
+ console.log('Rebuilding FTS index...');
10
+ const db = memoryDb.getDb();
11
+ db.exec('DROP TABLE IF EXISTS memories_fts');
12
+ db.exec(`CREATE VIRTUAL TABLE memories_fts USING fts5(content, structured_content, summary, tags, keywords)`);
13
+ const rows = db.prepare('SELECT id, content, structured_content, summary, tags, keywords FROM memories').all();
14
+ let count = 0;
15
+ const insert = db.prepare('INSERT INTO memories_fts(rowid, content, structured_content, summary, tags, keywords) VALUES (?, ?, ?, ?, ?, ?)');
16
+ for (const r of rows) {
17
+ try {
18
+ insert.run(r.id, memoryDb.tokenize(r.content || ''), memoryDb.tokenize(r.structured_content || ''), memoryDb.tokenize(r.summary || ''), memoryDb.tokenize(r.tags || ''), memoryDb.tokenize(r.keywords || ''));
19
+ count++;
20
+ } catch (e) {}
21
+ }
22
+ console.log(`FTS done: ${count}/${rows.length} indexed`);
23
+
24
+ memoryDb.closeDb();
25
+ }
26
+
27
+ main().catch(e => { console.error(e); process.exit(1); });