@hbarefoot/engram 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.
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/engram.js +421 -0
- package/dashboard/dist/assets/index-BHkLa5w_.css +1 -0
- package/dashboard/dist/assets/index-D9QR_Cnu.js +45 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/package.json +21 -0
- package/package.json +76 -0
- package/src/config/index.js +150 -0
- package/src/embed/index.js +249 -0
- package/src/export/static.js +396 -0
- package/src/extract/rules.js +233 -0
- package/src/extract/secrets.js +114 -0
- package/src/index.js +54 -0
- package/src/memory/consolidate.js +420 -0
- package/src/memory/context.js +346 -0
- package/src/memory/feedback.js +197 -0
- package/src/memory/recall.js +350 -0
- package/src/memory/store.js +626 -0
- package/src/server/mcp.js +668 -0
- package/src/server/rest.js +499 -0
- package/src/utils/id.js +9 -0
- package/src/utils/logger.js +79 -0
- package/src/utils/time.js +296 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
|
|
8
|
+
import { loadConfig, getDatabasePath, getModelsPath } from '../config/index.js';
|
|
9
|
+
import { initDatabase, createMemory, createMemoryWithDedup, getMemory, deleteMemory, getStats } from '../memory/store.js';
|
|
10
|
+
import { recallMemories, formatRecallResults } from '../memory/recall.js';
|
|
11
|
+
import { recordFeedback, getFeedbackStats } from '../memory/feedback.js';
|
|
12
|
+
import { generateContext } from '../memory/context.js';
|
|
13
|
+
import { validateContent } from '../extract/secrets.js';
|
|
14
|
+
import { extractMemory } from '../extract/rules.js';
|
|
15
|
+
import * as logger from '../utils/logger.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MCP Server for Engram
|
|
19
|
+
* Provides 4 tools: engram_remember, engram_recall, engram_forget, engram_status
|
|
20
|
+
*/
|
|
21
|
+
export class EngramMCPServer {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.db = null;
|
|
25
|
+
this.server = new Server(
|
|
26
|
+
{
|
|
27
|
+
name: 'engram',
|
|
28
|
+
version: '1.0.0'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
capabilities: {
|
|
32
|
+
tools: {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
this.setupToolHandlers();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize database connection
|
|
42
|
+
*/
|
|
43
|
+
initializeDatabase() {
|
|
44
|
+
if (!this.db) {
|
|
45
|
+
const dbPath = getDatabasePath(this.config);
|
|
46
|
+
this.db = initDatabase(dbPath);
|
|
47
|
+
logger.info('MCP Server database initialized', { path: dbPath });
|
|
48
|
+
}
|
|
49
|
+
return this.db;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Setup MCP tool handlers
|
|
54
|
+
*/
|
|
55
|
+
setupToolHandlers() {
|
|
56
|
+
// List available tools
|
|
57
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
58
|
+
return {
|
|
59
|
+
tools: [
|
|
60
|
+
{
|
|
61
|
+
name: 'engram_remember',
|
|
62
|
+
description: 'Store a memory/fact/preference/pattern that should be remembered across sessions. Use this when you learn something important about the user, their project, their preferences, infrastructure, or workflow patterns.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
content: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'The memory to store. Be specific and factual. Good: "User prefers Fastify over Express for Node.js APIs". Bad: "User likes stuff".'
|
|
69
|
+
},
|
|
70
|
+
category: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
enum: ['preference', 'fact', 'pattern', 'decision', 'outcome'],
|
|
73
|
+
description: 'Type of memory. preference=user likes/dislikes, fact=objective truth about their setup, pattern=recurring workflow, decision=choice they made and why, outcome=result of an action',
|
|
74
|
+
default: 'fact'
|
|
75
|
+
},
|
|
76
|
+
entity: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'What this memory is about (e.g., "nginx", "deployment", "coding-style", "project-api"). Helps with retrieval.'
|
|
79
|
+
},
|
|
80
|
+
confidence: {
|
|
81
|
+
type: 'number',
|
|
82
|
+
description: 'How confident you are this is accurate (0.0-1.0). Default 0.8. Use 1.0 for things the user explicitly stated. Use 0.5-0.7 for inferred preferences.',
|
|
83
|
+
default: 0.8
|
|
84
|
+
},
|
|
85
|
+
namespace: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Project or scope for this memory. Use "default" for general memories, or a project name for project-specific ones.',
|
|
88
|
+
default: 'default'
|
|
89
|
+
},
|
|
90
|
+
tags: {
|
|
91
|
+
type: 'array',
|
|
92
|
+
items: { type: 'string' },
|
|
93
|
+
description: 'Optional tags for categorization'
|
|
94
|
+
},
|
|
95
|
+
force: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
description: 'Bypass deduplication check. If true, memory will be stored even if a similar one exists. Default: false',
|
|
98
|
+
default: false
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
required: ['content']
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'engram_recall',
|
|
106
|
+
description: 'Retrieve relevant memories for the current context. Call this at the start of a session or when you need to remember something about the user, their project, or their preferences. Returns the most relevant memories ranked by similarity and recency.',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
query: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'What you want to remember. Can be a question ("what is their deployment setup?") or a topic ("docker configuration"). Be specific for better results.'
|
|
113
|
+
},
|
|
114
|
+
limit: {
|
|
115
|
+
type: 'number',
|
|
116
|
+
description: 'Maximum memories to return (1-20). Default 5. Keep low to avoid context pollution.',
|
|
117
|
+
default: 5
|
|
118
|
+
},
|
|
119
|
+
category: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
enum: ['preference', 'fact', 'pattern', 'decision', 'outcome'],
|
|
122
|
+
description: 'Optional: filter by memory type'
|
|
123
|
+
},
|
|
124
|
+
namespace: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Optional: filter by project/scope. Omit to search all namespaces.'
|
|
127
|
+
},
|
|
128
|
+
threshold: {
|
|
129
|
+
type: 'number',
|
|
130
|
+
description: 'Minimum relevance score (0.0-1.0). Default 0.3. Increase to get fewer, more relevant results.',
|
|
131
|
+
default: 0.3
|
|
132
|
+
},
|
|
133
|
+
time_filter: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
description: 'Filter memories by time range. Supports relative times like "3 days ago", "last week", or ISO dates.',
|
|
136
|
+
properties: {
|
|
137
|
+
after: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Start time - ISO date (2024-01-01) or relative (3 days ago, last week, yesterday)'
|
|
140
|
+
},
|
|
141
|
+
before: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'End time - ISO date or relative (today, now)'
|
|
144
|
+
},
|
|
145
|
+
period: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
enum: ['today', 'yesterday', 'this_week', 'last_week', 'this_month', 'last_month', 'this_year', 'last_year'],
|
|
148
|
+
description: 'Shorthand for common time periods'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
required: ['query']
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'engram_forget',
|
|
158
|
+
description: 'Remove a specific memory by ID. Use when a memory is outdated, incorrect, or the user asks you to forget something.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
memory_id: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'The ID of the memory to remove (returned by engram_recall)'
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
required: ['memory_id']
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'engram_feedback',
|
|
172
|
+
description: 'Provide feedback on a recalled memory to help improve future recall accuracy. Positive feedback increases a memory\'s relevance score; negative feedback decreases it.',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
memory_id: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
description: 'The ID of the memory to provide feedback on (returned by engram_recall)'
|
|
179
|
+
},
|
|
180
|
+
helpful: {
|
|
181
|
+
type: 'boolean',
|
|
182
|
+
description: 'Was this memory helpful in the current context? true = helpful, false = not helpful'
|
|
183
|
+
},
|
|
184
|
+
context: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
description: 'Optional: describe the context or query that prompted this feedback'
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
required: ['memory_id', 'helpful']
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'engram_context',
|
|
194
|
+
description: 'Generate a pre-formatted context block of relevant memories to inject into your system prompt or conversation. Use this at the start of a session to load relevant user context.',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
query: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
description: 'Optional query to filter relevant memories. If omitted, returns top memories by access frequency and recency.'
|
|
201
|
+
},
|
|
202
|
+
namespace: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'Namespace to pull context from (default: "default")',
|
|
205
|
+
default: 'default'
|
|
206
|
+
},
|
|
207
|
+
limit: {
|
|
208
|
+
type: 'number',
|
|
209
|
+
description: 'Maximum memories to include (1-25). Default 10.',
|
|
210
|
+
default: 10
|
|
211
|
+
},
|
|
212
|
+
format: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
enum: ['markdown', 'xml', 'json', 'plain'],
|
|
215
|
+
description: 'Output format. markdown=human-readable, xml=structured, json=programmatic, plain=raw text',
|
|
216
|
+
default: 'markdown'
|
|
217
|
+
},
|
|
218
|
+
include_metadata: {
|
|
219
|
+
type: 'boolean',
|
|
220
|
+
description: 'Include memory IDs and confidence scores in output',
|
|
221
|
+
default: false
|
|
222
|
+
},
|
|
223
|
+
categories: {
|
|
224
|
+
type: 'array',
|
|
225
|
+
items: { type: 'string' },
|
|
226
|
+
description: 'Filter by categories (e.g., ["preference", "fact"])'
|
|
227
|
+
},
|
|
228
|
+
max_tokens: {
|
|
229
|
+
type: 'number',
|
|
230
|
+
description: 'Approximate token budget. Will truncate to fit. Default 1000.',
|
|
231
|
+
default: 1000
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'engram_status',
|
|
238
|
+
description: 'Check Engram health and stats. Returns memory count, database size, embedding model status, and configuration.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Handle tool calls
|
|
249
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
250
|
+
try {
|
|
251
|
+
const { name, arguments: args } = request.params;
|
|
252
|
+
|
|
253
|
+
switch (name) {
|
|
254
|
+
case 'engram_remember':
|
|
255
|
+
return await this.handleRemember(args);
|
|
256
|
+
case 'engram_recall':
|
|
257
|
+
return await this.handleRecall(args);
|
|
258
|
+
case 'engram_forget':
|
|
259
|
+
return await this.handleForget(args);
|
|
260
|
+
case 'engram_feedback':
|
|
261
|
+
return await this.handleFeedback(args);
|
|
262
|
+
case 'engram_context':
|
|
263
|
+
return await this.handleContext(args);
|
|
264
|
+
case 'engram_status':
|
|
265
|
+
return await this.handleStatus(args);
|
|
266
|
+
default:
|
|
267
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logger.error('Tool execution error', { error: error.message, stack: error.stack });
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: `Error: ${error.message}`
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Handle engram_remember tool
|
|
285
|
+
*/
|
|
286
|
+
async handleRemember(args) {
|
|
287
|
+
const db = this.initializeDatabase();
|
|
288
|
+
const { content, category, entity, confidence, namespace, tags, force } = args;
|
|
289
|
+
|
|
290
|
+
logger.info('Remember requested', { category, entity, namespace, force });
|
|
291
|
+
|
|
292
|
+
// Validate content for secrets
|
|
293
|
+
const validation = validateContent(content, {
|
|
294
|
+
autoRedact: this.config.security?.secretDetection !== false
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!validation.valid) {
|
|
298
|
+
const errorMsg = `Cannot store memory: ${validation.errors.join(', ')}`;
|
|
299
|
+
logger.warn('Memory rejected due to secrets', { errors: validation.errors });
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: errorMsg
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Auto-extract category and entity if not provided
|
|
312
|
+
let memoryData = {
|
|
313
|
+
content: validation.content, // Use potentially redacted content
|
|
314
|
+
category: category || 'fact',
|
|
315
|
+
entity: entity,
|
|
316
|
+
confidence: confidence !== undefined ? confidence : 0.8,
|
|
317
|
+
namespace: namespace || 'default',
|
|
318
|
+
tags: tags || [],
|
|
319
|
+
source: 'mcp'
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Extract category/entity if not provided
|
|
323
|
+
if (!entity || !category) {
|
|
324
|
+
const extracted = extractMemory(validation.content, {
|
|
325
|
+
source: 'mcp',
|
|
326
|
+
namespace: namespace || 'default'
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (!entity) {
|
|
330
|
+
memoryData.entity = extracted.entity;
|
|
331
|
+
}
|
|
332
|
+
if (!category) {
|
|
333
|
+
memoryData.category = extracted.category;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Generate embedding
|
|
338
|
+
try {
|
|
339
|
+
const { generateEmbedding } = await import('../embed/index.js');
|
|
340
|
+
const modelsPath = getModelsPath(this.config);
|
|
341
|
+
const embedding = await generateEmbedding(validation.content, modelsPath);
|
|
342
|
+
memoryData.embedding = embedding;
|
|
343
|
+
logger.debug('Embedding generated for memory');
|
|
344
|
+
} catch (error) {
|
|
345
|
+
logger.warn('Failed to generate embedding, storing without it', { error: error.message });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Store memory with deduplication check
|
|
349
|
+
const result = createMemoryWithDedup(db, memoryData, { force: force || false });
|
|
350
|
+
|
|
351
|
+
let responseText;
|
|
352
|
+
|
|
353
|
+
switch (result.status) {
|
|
354
|
+
case 'duplicate':
|
|
355
|
+
responseText = `Similar memory already exists (${(result.similarity * 100).toFixed(1)}% match)\n\nExisting ID: ${result.id}\nExisting content: ${result.existingContent}\n\nUse force: true to store anyway.`;
|
|
356
|
+
logger.info('Duplicate memory rejected', { existingId: result.id, similarity: result.similarity });
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
case 'merged':
|
|
360
|
+
responseText = `Memory merged with existing (${(result.similarity * 100).toFixed(1)}% match)\n\nID: ${result.id}\nCategory: ${result.memory.category}\nEntity: ${result.memory.entity || 'none'}\nConfidence: ${result.memory.confidence}\nNamespace: ${result.memory.namespace}\n\nMerged content: ${result.memory.content}`;
|
|
361
|
+
logger.info('Memory merged', { id: result.id, similarity: result.similarity });
|
|
362
|
+
break;
|
|
363
|
+
|
|
364
|
+
case 'created':
|
|
365
|
+
default:
|
|
366
|
+
responseText = `Memory stored successfully!\n\nID: ${result.id}\nCategory: ${result.memory.category}\nEntity: ${result.memory.entity || 'none'}\nConfidence: ${result.memory.confidence}\nNamespace: ${result.memory.namespace}`;
|
|
367
|
+
logger.info('Memory stored', { id: result.id, category: result.memory.category });
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (validation.warnings && validation.warnings.length > 0) {
|
|
372
|
+
responseText += `\n\nWarnings: ${validation.warnings.join(', ')}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: 'text',
|
|
379
|
+
text: responseText
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Handle engram_recall tool
|
|
387
|
+
*/
|
|
388
|
+
async handleRecall(args) {
|
|
389
|
+
const db = this.initializeDatabase();
|
|
390
|
+
const { query, limit = 5, category, namespace, threshold = 0.3, time_filter } = args;
|
|
391
|
+
|
|
392
|
+
logger.info('Recall requested', { query, limit, category, namespace, threshold, time_filter });
|
|
393
|
+
|
|
394
|
+
const modelsPath = getModelsPath(this.config);
|
|
395
|
+
|
|
396
|
+
// Recall memories
|
|
397
|
+
const memories = await recallMemories(
|
|
398
|
+
db,
|
|
399
|
+
query,
|
|
400
|
+
{ limit, category, namespace, threshold, time_filter },
|
|
401
|
+
modelsPath
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// Format results
|
|
405
|
+
const formattedResults = formatRecallResults(memories);
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
content: [
|
|
409
|
+
{
|
|
410
|
+
type: 'text',
|
|
411
|
+
text: formattedResults
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle engram_forget tool
|
|
419
|
+
*/
|
|
420
|
+
async handleForget(args) {
|
|
421
|
+
const db = this.initializeDatabase();
|
|
422
|
+
const { memory_id } = args;
|
|
423
|
+
|
|
424
|
+
logger.info('Forget requested', { memory_id });
|
|
425
|
+
|
|
426
|
+
// Check if memory exists
|
|
427
|
+
const memory = getMemory(db, memory_id);
|
|
428
|
+
|
|
429
|
+
if (!memory) {
|
|
430
|
+
return {
|
|
431
|
+
content: [
|
|
432
|
+
{
|
|
433
|
+
type: 'text',
|
|
434
|
+
text: `Memory not found: ${memory_id}`
|
|
435
|
+
}
|
|
436
|
+
]
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Delete the memory
|
|
441
|
+
const deleted = deleteMemory(db, memory_id);
|
|
442
|
+
|
|
443
|
+
if (deleted) {
|
|
444
|
+
logger.info('Memory deleted', { id: memory_id });
|
|
445
|
+
return {
|
|
446
|
+
content: [
|
|
447
|
+
{
|
|
448
|
+
type: 'text',
|
|
449
|
+
text: `Memory deleted successfully: ${memory_id}\n\nContent: ${memory.content}`
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
};
|
|
453
|
+
} else {
|
|
454
|
+
return {
|
|
455
|
+
content: [
|
|
456
|
+
{
|
|
457
|
+
type: 'text',
|
|
458
|
+
text: `Failed to delete memory: ${memory_id}`
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Handle engram_feedback tool
|
|
467
|
+
*/
|
|
468
|
+
async handleFeedback(args) {
|
|
469
|
+
const db = this.initializeDatabase();
|
|
470
|
+
const { memory_id, helpful, context } = args;
|
|
471
|
+
|
|
472
|
+
logger.info('Feedback requested', { memory_id, helpful, context });
|
|
473
|
+
|
|
474
|
+
// Check if memory exists
|
|
475
|
+
const memory = getMemory(db, memory_id);
|
|
476
|
+
|
|
477
|
+
if (!memory) {
|
|
478
|
+
return {
|
|
479
|
+
content: [
|
|
480
|
+
{
|
|
481
|
+
type: 'text',
|
|
482
|
+
text: `Memory not found: ${memory_id}`
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Record the feedback
|
|
489
|
+
const result = recordFeedback(db, memory_id, helpful, context);
|
|
490
|
+
|
|
491
|
+
const helpfulText = helpful ? 'helpful' : 'not helpful';
|
|
492
|
+
let responseText = `Feedback recorded: memory marked as ${helpfulText}\n\n`;
|
|
493
|
+
responseText += `Memory ID: ${memory_id}\n`;
|
|
494
|
+
responseText += `Feedback Score: ${result.feedbackScore.toFixed(2)} (${result.helpfulCount} helpful, ${result.unhelpfulCount} unhelpful)\n`;
|
|
495
|
+
|
|
496
|
+
if (result.confidenceAdjusted) {
|
|
497
|
+
responseText += `\nConfidence adjusted to: ${result.newConfidence.toFixed(2)}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
logger.info('Feedback recorded', {
|
|
501
|
+
memoryId: memory_id,
|
|
502
|
+
helpful,
|
|
503
|
+
feedbackScore: result.feedbackScore,
|
|
504
|
+
confidenceAdjusted: result.confidenceAdjusted
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
content: [
|
|
509
|
+
{
|
|
510
|
+
type: 'text',
|
|
511
|
+
text: responseText
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Handle engram_context tool
|
|
519
|
+
*/
|
|
520
|
+
async handleContext(args) {
|
|
521
|
+
const db = this.initializeDatabase();
|
|
522
|
+
const {
|
|
523
|
+
query,
|
|
524
|
+
namespace = 'default',
|
|
525
|
+
limit = 10,
|
|
526
|
+
format = 'markdown',
|
|
527
|
+
include_metadata = false,
|
|
528
|
+
categories,
|
|
529
|
+
max_tokens = 1000
|
|
530
|
+
} = args;
|
|
531
|
+
|
|
532
|
+
logger.info('Context requested', { query, namespace, limit, format });
|
|
533
|
+
|
|
534
|
+
const modelsPath = getModelsPath(this.config);
|
|
535
|
+
|
|
536
|
+
// Generate context
|
|
537
|
+
const result = await generateContext(db, {
|
|
538
|
+
query,
|
|
539
|
+
namespace,
|
|
540
|
+
limit: Math.min(limit, 25),
|
|
541
|
+
format,
|
|
542
|
+
include_metadata,
|
|
543
|
+
categories,
|
|
544
|
+
max_tokens
|
|
545
|
+
}, modelsPath);
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
content: [
|
|
549
|
+
{
|
|
550
|
+
type: 'text',
|
|
551
|
+
text: result.content
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Handle engram_status tool
|
|
559
|
+
*/
|
|
560
|
+
async handleStatus() {
|
|
561
|
+
const db = this.initializeDatabase();
|
|
562
|
+
|
|
563
|
+
logger.info('Status requested');
|
|
564
|
+
|
|
565
|
+
// Get database stats
|
|
566
|
+
const stats = getStats(db);
|
|
567
|
+
|
|
568
|
+
// Get model info
|
|
569
|
+
const modelsPath = getModelsPath(this.config);
|
|
570
|
+
let modelInfo;
|
|
571
|
+
try {
|
|
572
|
+
const { getModelInfo } = await import('../embed/index.js');
|
|
573
|
+
modelInfo = getModelInfo(modelsPath);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
modelInfo = {
|
|
576
|
+
name: 'unknown',
|
|
577
|
+
available: false,
|
|
578
|
+
error: error.message
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Build status response
|
|
583
|
+
const statusText = `Engram Status
|
|
584
|
+
|
|
585
|
+
📊 Memory Statistics:
|
|
586
|
+
- Total memories: ${stats.total}
|
|
587
|
+
- With embeddings: ${stats.withEmbeddings}
|
|
588
|
+
- By category: ${Object.entries(stats.byCategory).map(([k, v]) => `${k}=${v}`).join(', ')}
|
|
589
|
+
- By namespace: ${Object.entries(stats.byNamespace).map(([k, v]) => `${k}=${v}`).join(', ')}
|
|
590
|
+
|
|
591
|
+
🤖 Embedding Model:
|
|
592
|
+
- Name: ${modelInfo.name}
|
|
593
|
+
- Available: ${modelInfo.available ? 'Yes' : 'No'}
|
|
594
|
+
- Cached: ${modelInfo.cached ? 'Yes' : 'No'}
|
|
595
|
+
- Size: ${modelInfo.sizeMB} MB
|
|
596
|
+
- Path: ${modelInfo.path}
|
|
597
|
+
|
|
598
|
+
⚙️ Configuration:
|
|
599
|
+
- Data directory: ${this.config.dataDir}
|
|
600
|
+
- Default namespace: ${this.config.defaults.namespace}
|
|
601
|
+
- Recall limit: ${this.config.defaults.recallLimit}
|
|
602
|
+
- Confidence threshold: ${this.config.defaults.confidenceThreshold}
|
|
603
|
+
- Secret detection: ${this.config.security.secretDetection ? 'Enabled' : 'Disabled'}
|
|
604
|
+
`;
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
content: [
|
|
608
|
+
{
|
|
609
|
+
type: 'text',
|
|
610
|
+
text: statusText
|
|
611
|
+
}
|
|
612
|
+
]
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Start the MCP server
|
|
618
|
+
*/
|
|
619
|
+
async start() {
|
|
620
|
+
logger.info('Starting Engram MCP server...');
|
|
621
|
+
|
|
622
|
+
const transport = new StdioServerTransport();
|
|
623
|
+
await this.server.connect(transport);
|
|
624
|
+
|
|
625
|
+
logger.info('Engram MCP server started successfully');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Close the server
|
|
630
|
+
*/
|
|
631
|
+
async close() {
|
|
632
|
+
if (this.db) {
|
|
633
|
+
this.db.close();
|
|
634
|
+
logger.info('Database connection closed');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
await this.server.close();
|
|
638
|
+
logger.info('MCP server closed');
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Start the MCP server
|
|
644
|
+
*/
|
|
645
|
+
export async function startMCPServer(configPath) {
|
|
646
|
+
try {
|
|
647
|
+
const config = loadConfig(configPath);
|
|
648
|
+
const server = new EngramMCPServer(config);
|
|
649
|
+
|
|
650
|
+
// Handle shutdown gracefully
|
|
651
|
+
process.on('SIGINT', async () => {
|
|
652
|
+
logger.info('Received SIGINT, shutting down...');
|
|
653
|
+
await server.close();
|
|
654
|
+
process.exit(0);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
process.on('SIGTERM', async () => {
|
|
658
|
+
logger.info('Received SIGTERM, shutting down...');
|
|
659
|
+
await server.close();
|
|
660
|
+
process.exit(0);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
await server.start();
|
|
664
|
+
} catch (error) {
|
|
665
|
+
logger.error('Failed to start MCP server', { error: error.message, stack: error.stack });
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
}
|